A small library to help you avoid boilerplate for standard CRUD actions, while improving your controllers' readibility.
Installation
Add this line to your application's Gemfile:
gem 'resourcerer'
And then execute:
$ bundle
Or install it yourself as:
$ gem install resourcerer
Usage
In the simplest scenario you'll just use it to define a resource in the controller:
class BandsController < ApplicationController
resource :band
end
Now every time you call band
in your controller or view, it will look for an
ID and try to perform Band.find(id)
. If an ID parameter isn't found, it will
call Band.new(band_params)
. The result will be memoized in a
@resourcerer_band
instance variable.
Example
Here's what a standard Rails CRUD controller using Resourcerer might look like:
class BandsController < ApplicationController
resource :band do
permit [:name, :genre]
end
def create
if band.save
redirect_to band_path(band)
else
render :new
end
end
def update
if band.save
redirect_to band_path(band)
else
render :edit
end
end
def destroy
band.destroy
redirect_to bands_path
end
end
That's way less code than usual! 😃
Under the Hood
The default resolving workflow is pretty powerful and customizable. It could be expressed with the following pseudocode:
def fetch(scope, id)
instance = id ? find(id, scope) : build(attrs, scope)
instance.tap { instance.assign_attributes(attrs) if assign? }
end
def id
params[:band_id] || params[:id]
end
def find(id, scope)
scope.find(id)
end
def build(params, scope)
scope.new(params) # Band.new(params)
end
def scope
model # Band
end
def model
:band.classify.constantize # Band
end
def assign?
action_name == 'update'
end
def attrs
if respond_to?(:band_params, true) && !request.get?
band_params
else
{}
end
end
The resource is lazy, so it won't do anyband until the method is called.
Configuration
It is possible to override each step with options. The acceptable options to the
resource
macro are:
id
In order to fetch a resource Resourcerer relies on the presence of an ID:
# Default Behavior
resource :band, id: ->{ params[:band_id] || params[:id] }
You can override any option's default behavior by passing in a Proc
:
resource :band, id: ->{ 42 }
Passing lambdas might not always be fun, so most options provide shortcuts that might help make life easier:
resource :band, id: :custom_band_id
# same as
resource :band, id: ->{ params[:custom_band_id] }
resource :band, id: [:try_this_id, :or_maybe_that_id]
# same as
resource :band, id: ->{ params[:try_this_id] || params[:or_maybe_that_id] }
find
If an ID was provided, Resourcerer will try to find the model:
# Default Behavior
resource :band, find: -> (id, scope) { scope.find(id) }
Where scope
is a model scope, like Band
or User.active
or
Post.published
. There's even a convenient shortcut for cases where the ID is
actually something else:
resource :band, find_by: :slug
# same as
resource :band, find: ->(slug, scope){ scope.find_by!(slug: slug) }
build
When an ID is not present, Resourcerer tries to build an object for you:
# Default Behavior
resource :band, build: ->(attrs, scope){ scope.new(band_params) }
attrs
This option is responsible for calulating params before passing them to the
build step. The default behavior was modeled with Strong Parameters in mind and
is somewhat smart: it calls the band_params
controller method if it's
available and the request method is not GET
. In all other cases it produces
an empty hash.
You can easily specify which controller method you want it to call instead of
band_params
, or just provide your own logic:
resource :band, attrs: :custom_band_params
resource :other_band, attrs: ->{ { foo: "bar" } }
private
def custom_band_params
params.require(:band).permit(:name, :genre)
end
Using the default model name conventions? permit
can do that for you:
resource :band, permit: [:name, :genre]
collection
Defines the scope that's used in find
and build
steps:
resource :band, collection: ->{ current_user.bands }
model
Allows you to specify the model class to use:
resource :band, model: ->{ AnotherBand }
resource :band, model: AnotherBand
resource :band, model: "AnotherBand"
resource :band, model: :another_band
assign
and assign?
Allows you to specify whether the attributes should be assigned:
resource :band, assign?: false
resource :band, assign?: [:edit, :update]
resource :band, assign?: ->{ current_user.admin? }
and also how to assign them:
resource :band, assign: ->(band, attrs) { band.set_info(attrs) }
Advanced Configuration with resourcerer_config
You can define configuration presets with the resourcerer_config
method to reuse
them later in different resource definitions.
resourcerer_config :cool_find, find: ->{ very_cool_find_code }
resourcerer_config :cool_build, build: ->{ very_cool_build_code }
resource :band, using: [:cool_find, :cool_build]
resource :another_band, using: :cool_build
Options that are passed to resource
will take precedence over the presets.
Decorators or Presenters (like draper)
If you use decorators, you'll be able to avoid even more boilerplate if you throw presenter_rails in the mix:
class BandController < ApplicationController
resource(:band, permit: :name)
present(:band) { band.decorate }
def create
if band.save
redirect_to(band)
else
render :new
end
end
def update
if band.save
redirect_to(band)
else
render :edit
end
end
end
Comparison with Decent Exposure.
Resourcerer is heavily inspired on Decent Exposure, but it attempts to be simpler and more flexible by not focusing on exposing variables to the view context.
Similarities
Both allow you to find or initialize a model and assign attributes, removing the boilerplate from most CRUD actions.
Differences
Resourcerer does not expose an object to the view in any way, nor deal with decoratation. It also provides better support for strong parameters.
Special Thanks
Resourcerer is based on DecentExposure.