No release in over 3 years
Low commit activity in last 3 years
A module providing the seven standard resource actions & responses for an ActiveRecord :model_type & :collection.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

Runtime

>= 7.0.0
 Project Readme

Typical Situation Spec CI

The missing Ruby on Rails ActionController REST API mixin. Originally forked from https://github.com/mars/typical_situation

A Ruby mixin (module) providing the seven standard resource actions & responses for an ActiveRecord :model_type & :collection.

Installation

Tested in:

  • Rails 7.0
  • Rails 7.1
  • Rails 8.0

Against Ruby versions:

  • 3.2
  • 3.3
  • 3.4

Add to your Gemfile:

gem 'typical_situation'

Legacy Versions: For Rails 4.x/5.x/6.x support, see older versions of this gem. Ruby 3.0+ is required.

Usage

Define your model and methods

Basic usage is to declare the typical_situation, and then two required helper methods. Everything else is handled automatically.

class PostsController < ApplicationController
  include TypicalSituation

  # Symbolized, underscored version of the model to use as the resource.
  typical_situation :post # => maps to the Post model

  private

  # The collection of model instances.
  def collection
    current_user.posts
  end

  # Find a model instance by ID.
  def find_in_collection(id)
    collection.find_by_id(id)
  end
end

There are two alternative helper methods:

Typical REST

The typical REST helper is an alias for typical_situation, and defines the 7 standard REST endpoints: index, show, new, create, edit, update, destroy.

class PostsController < ApplicationController
  include TypicalSituation

  typical_rest :post

  ...
end

Typical CRUD

Sometimes you don't need all seven endpoints, and just need standard CRUD. The typical CRUD helper defines the 4 standard CRUD endpoints: create, show, update, destroy.

class PostsController < ApplicationController
  include TypicalSituation

  typical_crud :post

  ...
end

Customizing defined endpoints

You can also define only the endpoints you want by passing an only flag to typical_situation:

class PostsController < ApplicationController
  include TypicalSituation

  typical_situation :post, only: [:index, :show]

  ...
end

Customize by overriding highly composable methods

TypicalSituation is composed of a library of common functionality, which can all be overridden in individual controllers. Express what is different & special about each controller, instead of repeating boilerplate.

The library is split into modules:

  • identity - required definitions of the model & how to find it
  • actions - high-level controller actions
  • operations - loading, changing, & persisting the model
  • permissions - handling authorization to records and actions
  • responses - HTTP responses & redirects

Common Customization Hooks

Scoped Collections - Filter the collection based on user permissions or other criteria:

def scoped_resource
  if current_user.admin?
    collection
  else
    collection.where(published: true)
  end
end

Custom Lookup - Use different attributes for finding resources:

def find_resource(param)
  collection.find_by!(slug: param)
end

Custom Redirects - Control where users go after actions:

def after_resource_created_path(resource)
  { action: :index }
end

def after_resource_updated_path(resource)
  edit_resource_path(resource)
end

def after_resource_destroyed_path(resource)
  { action: :index }
end

Sorting - Set default sorting for index pages:

def default_sorting_attribute
  :created_at
end

def default_sorting_direction
  :desc
end

Pagination - Bring your own pagination solution:

# Kaminari
def paginate_resources(resources)
  resources.page(params[:page]).per(params[:per_page] || 25)
end

# will_paginate
def paginate_resources(resources)
  resources.paginate(page: params[:page], per_page: params[:per_page] || 25)
end

 # Custom pagination
 def paginate_resources(resources)
   resources.limit(20).offset((params[:page].to_i - 1) * 20)
end

Strong Parameters - Control which parameters are allowed for create and update operations:

class PostsController < ApplicationController
  include TypicalSituation
  typical_situation :post

  private

  # Only allow title and content for new posts
  def permitted_create_params
    [:title, :content]
  end

  # Allow title, content, and published for updates
  def permitted_update_params
    [:title, :content, :published]
  end
end

By default, TypicalSituation permits all parameters (permit!) when these methods return nil or an empty array. Override them to restrict parameters for security.

Flash Messages

Add to config/locales/en.yml:

en:
  typical_situation:
    flash:
      create:
        success: "%{resource} was successfully created"
      update:
        success: "%{resource} was successfully updated"
      destroy:
        success: "%{resource} was successfully deleted"

Custom messages per resource:

en:
  posts:
    flash:
      create:
        success: "Your blog post is now live"

Override translation helper:

def translate_with_resource(key)
  model_key = "#{resource_name}.flash.#{key}"
  if I18n.exists?(model_key)
    I18n.t(model_key, resource: resource_name.humanize)
  else
    I18n.t("typical_situation.flash.#{key}", resource: resource_name.humanize)
  end
end

Custom flash logic:

def set_success_flash(action)
  case action
  when :create
    flash[:notice] = "Your #{resource_name.humanize.downcase} is live"
  when :destroy
    flash[:warning] = translate_with_resource("#{action}.success")
  end
end

Authorization

Control access to resources by overriding the authorized? method:

class PostsController < ApplicationController
  include TypicalSituation
  typical_situation :post

  private

  def authorized?(action, resource = nil)
    case action
    when :destroy, :update, :edit
      resource&.user == current_user || current_user&.admin?
    when :show
      resource&.published? || resource&.user == current_user
    else
      true
    end
  end
end

You can also customize the response when authorization is denied:

def respond_as_forbidden
  redirect_to login_path, alert: "Access denied"
end
CanCanCan
def authorized?(action, resource = nil)
  can?(action, resource || model_class)
end
Pundit
def authorized?(action, resource = nil)
  policy(resource || model_class).public_send("#{action}?")
end

Serialization

Under the hood TypicalSituation calls to_json on your ActiveRecord models. This isn't always the optimal way to serialize resources, though, and so TypicalSituation offers a simple means of overriding the base Serialization --- either on an individual controller, or for your entire application.

Alba
class MockApplePieResource
  include Alba::Resource

  attributes :id, :ingredients
  
  association :grandma, resource: GrandmaResource
end

class MockApplePiesController < ApplicationController
  include TypicalSituation
  typical_situation :mock_apple_pie

  private

  def serializable_resource(resource)
    MockApplePieResource.new(resource).serialize
  end

  def collection
    current_user.mock_apple_pies
  end

  def find_in_collection(id)
    collection.find_by_id(id)
  end
end
ActiveModelSerializers
class MockApplePieIndexSerializer < ActiveModel::Serializer
  attributes :id, :ingredients
end

module TypicalSituation
  module Operations
    def serializable_resource(resource)
      if action_name == "index"
        ActiveModelSerializers::SerializableResource.new(
          resource,
          each_serializer: MockApplePieIndexSerializer
        )
      else
        ActiveModelSerializers::SerializableResource.new(resource)
      end
    end
  end
end
Fast JSON API
class MockApplePieSerializer
  include FastJsonapi::ObjectSerializer
  attributes :ingredients
  belongs_to :grandma
end

class MockApplePiesController < ApplicationController
  include TypicalSituation

  def serializable_resource(resource)
    MockApplePieSerializer.new(resource).serializable_hash
  end
end

Development

After checking out the repo, run bin/setup to install dependencies.

Local Setup

  1. Clone the repository
  2. Install dependencies:
    bundle install
  3. Install appraisal gemfiles for testing across Rails versions:
    bundle exec appraisal install

Running Tests

Tests are written using RSpec and are setup to use Appraisal to run tests over multiple Rails versions.

Run all tests across all supported Rails versions:

bundle exec appraisal rspec

Run tests for a specific Rails version:

bundle exec appraisal rails_7.0 rspec
bundle exec appraisal rails_7.1 rspec
bundle exec appraisal rails_8.0 rspec

Run specific test files:

bundle exec rspec spec/path/to/spec.rb
bundle exec appraisal rails_7.0 rspec spec/path/to/spec.rb

Linting and Formatting

This project uses Standard Ruby for code formatting and linting.

Check for style violations:

bundle exec standardrb

Automatically fix style violations:

bundle exec standardrb --fix

Run both linting and tests (the default rake task):

bundle exec rake

Console

Start an interactive console to experiment with the gem:

bundle exec irb -r typical_situation

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/typical_situation.

License

The gem is available as open source under the terms of the MIT License.


Built by Apsis

apsis

typical_situation was built by Apsis Labs. We love sharing what we build! Check out our other libraries on Github, and if you like our work you can hire us to build your vision.