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
- 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 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
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.