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
endThere 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
...
endTypical 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
...
endCustomizing 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]
...
endCustomize 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
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.
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
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
endAuthorization
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
endYou can also customize the response when authorization is denied:
def respond_as_forbidden
redirect_to login_path, alert: "Access denied"
endCanCanCan
def authorized?(action, resource = nil)
can?(action, resource || model_class)
endPundit
def authorized?(action, resource = nil)
policy(resource || model_class).public_send("#{action}?")
endSerialization
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
endActiveModelSerializers
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
endFast 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
endDevelopment
After checking out the repo, run bin/setup to install dependencies.
Local Setup
- Clone the repository
- Install dependencies:
bundle install
- 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 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.rbLinting and Formatting
This 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 rakeConsole
Start an interactive console to experiment with the gem:
bundle exec irb -r typical_situationContributing
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
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.