CrudResponder
Embrace CRUD interface for your models
We should keep controllers skinny and don't repeat ourself, right? But this scaffolded action doesn't look very DRY:
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: 'Post was successfully created.' }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
You can make it simpler if you don't need to respond to anything but html, or use responders
gem.
Let's assume we need only html and also add flash message on error:
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: 'Post was successfully updated.' }
else
flash[:alert] = "Error updating post: #{@post.errors.full_messages.to_sentence}"
render :edit
end
end
But it's still not DRY because we have to repeat this code in every controller, and even a different action of the same controller:
def update
@post = Post.find(params[:id])
if @post.update(post_params)
redirect_to @post, notice: 'Post was successfully created.'
else
flash[:alert] = "Error creating post: #{@post.errors.full_messages.to_sentence}"
render :new
end
end
But the only differences between these actions are:
- Method called on a model
- Action on success
- Action on error
- Text in flash message on success
- Text in flash message on error
And they are repeated over and over again. Let's extract these differences:
def create
@post = Post.new(post_params)
crud_respond @post # will call save, set flashes and redirects/render appropriately
end
def update
@post = Post.find(params[:id])
@post.assign_attributes(post_params)
crud_respond @post # also call save
end
def destroy
@post = Post.find(params[:id])
crud_respond @post # call destroy because called from destroy action
end
Method to call on the object determined by the name of controller's action.
Installation
Add this line to your application's Gemfile:
gem 'crud_responder'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install crud_responder
Generate initial files:
$ rails g crud_responder:install
Usage
Include CrudResponder
into your ApplicationController
(or whatever base controller you have). Note: generator already has included CrudResponder to Application controller.
class ApplicationController
include CrudResponder
end
Use crud_respond
method with object you need to create, update or destroy. Optionally, you can pass options to this method to override default redirect and render actions
def create
@post = Post.new(post_params)
crud_respond @post, success_url: root_path, error_action: :custom_new_action
# will redirect to root_path in case of success or render :custom_new_action otherwise
end
By default you will be redirected to object show, objects index or back in this order. And render :new when creating object and :edit when updating.
You can specify error_url
instead of error_action
to be redirected instead of action render in case of error:
def create
@post = Post.new(post_params)
crud_respond @post, success_url: root_path, error_url: 'https://google.com'
# will redirect to root_path in case of success or to https://google.com otherwise
end
You can specify custom flash messages:
def create
@post = Post.new(post_params)
crud_respond @post, success_message: 'Yep :)', error_message: 'Nope :('
# will render flash 'Yep :)' on success or 'Nope :(' on failure instead of default ones
end
You can also specify per-controller default options:
private
def crud_responder_default_options
{ success_url: root_path }
# will redirect to `root_path` for every successful request in this controller
end
If you need to create a bunch of objects (not just one) you can create wrapper class with same interface. For example, uploading multiple files:
# app/controllers/media_items_controller.rb
class MediaItemsController < ApplicationController
def create_multiple
@media_item_multiple = MediaItem::CreateMultiple.new(media_item_create_multiple_params)
crud_respond @media_item_multiple, success_url: media_items_path
end
private
def media_item_create_multiple_params
params.require(:media_item_create_multiple).permit(:description, files: [])
end
end
# app/models/media_item/create_multiple.rb
class MediaItem::CreateMultiple
include ActiveModel::Model
attr_accessor :description, :files
validates :files, presence: true
def save
return false unless valid?
ActiveRecord::Base.transaction do
files.each do |file|
MediaItem.create!(description: description, file: file)
end
end
true
rescue ActiveRecord::RecordInvalid => e
errors.add(:base, e.to_s)
false
end
end
Now your controllers are skinny again! Also, you are forced to think in terms of CRUD interface to models and REST to controllers.
TODO
- Support for pure API controllers (which is much simpler)
- Testing
Note about pure API controllers. I already use this idea:
# somewhere in base controller for API
protected
def result(object, method = :save, options = {})
method = :destroy if caller[0][/`.*'/][1..-2] == 'destroy'
serializable_attrs = *options.fetch(:serializable_attrs, :id)
if object.send method
object_json(object, serializable_attrs)
respond_with object, location: (method == :destroy ? nil : object_url(object))
else
fail UnprocessableEntityError, object.errors.full_messages.to_sentence
end
end
def object_url(object)
polymorphic_url(object)
rescue NoMethodError
"there is no show route for #{object.class.name} object"
end
def object_json(object, serializable_attrs)
object.instance_exec(serializable_attrs) do |attrs|
@_s_attrs = attrs
def to_json(*) # don't want to return the whole object on create/update
hash = {}
@_s_attrs.each do |attr|
hash[attr] = try(attr) if try(attr)
end
hash.any? ? JSON.generate(hash) : ''
end
end
end
I'm thinking of extracting this as well plus adding support for ActiveModelSerializers. Any feedback is welcome.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/olegantonyan/crud_responder. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.