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.
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.
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
endThere are two alternative helper methods:
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
...
endSometimes 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
...
endYou 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]
...
endTypicalSituation 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
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
endCustom Lookup - Use different attributes for finding resources:
def find_resource(param)
collection.find_by!(slug: param)
endCustom 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 }
endSorting - Set default sorting for index pages:
def default_sorting_attribute
:created_at
end
def default_sorting_direction
:desc
endPagination - 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)
endStrong 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
endBy default, TypicalSituation permits all parameters (permit!) when these methods return nil or an empty array. Override them to restrict parameters for security.
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
endCustom 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
endControl 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
endYou can also customize the response when authorization is denied:
def respond_as_forbidden
redirect_to login_path, alert: "Access denied"
enddef authorized?(action, resource = nil)
can?(action, resource || model_class)
enddef authorized?(action, resource = nil)
policy(resource || model_class).public_send("#{action}?")
endUnder 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.
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
endclass 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
endclass 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
endAfter checking out the repo, run bin/setup to install dependencies.
- Clone the repository
- Install dependencies:
bundle install
- Install appraisal gemfiles for testing across Rails versions:
bundle exec appraisal install
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 rspecRun 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 rspecRun specific test files:
bundle exec rspec spec/path/to/spec.rb
bundle exec appraisal rails_7.0 rspec spec/path/to/spec.rbThis project uses Standard Ruby for code formatting and linting.
Check for style violations:
bundle exec standardrbAutomatically fix style violations:
bundle exec standardrb --fixRun both linting and tests (the default rake task):
bundle exec rakeStart an interactive console to experiment with the gem:
bundle exec irb -r typical_situationBug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/typical_situation.
The gem is available as open source under the terms of the MIT License.
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.
