garden_variety
Delightfully boring Rails controllers. One of the superb advantages of Ruby on Rails is convention over configuration. Opinionated default behavior can decrease development time, and increase application robustness (less custom code == less that can go wrong). In service of this principle, garden_variety provides reasonable default controller actions, with care to allow easy override.
garden_variety also uses the excellent Pundit gem to isolate authorization concerns. If you're unfamiliar with Pundit, see its documentation for an explanation of policy objects and how they help controller actions stay DRY and boring.
As an example, this controller using garden_variety
...
class PostsController < ApplicationController
garden_variety
end
...is equivalent to the following conventional implementation:
class PostsController < ApplicationController
def index
(resource_class)
self.resources = policy_scope(list_resources)
end
def show
self.resource = find_resource
(resource)
end
def new
self.resource = new_resource
(resource)
end
def create
self.resource = vest(new_resource)
if resource.save
redirect_to resource
else
render :new
end
end
def edit
self.resource = find_resource
(resource)
end
def update
self.resource = vest(find_resource)
if resource.save
redirect_to resource
else
render :edit
end
end
def destroy
self.resource = find_resource
(resource)
resource.destroy!
redirect_to action: :index
end
private
def resource_class
Post
end
def resources
@posts
end
def resources=(models)
@posts = models
end
def resource
@post
end
def resource=(model)
@post = model
end
def list_resources
resource_class.all
end
def find_resource
resource_class.find(params[:id])
end
def new_resource
resource_class.new
end
def vest(model)
(model)
model.assign_attributes(permitted_attributes(model))
model
end
end
The implementations of the resource_class
and resource
/ resources
accessor methods are generated based on the controller name. They can
be altered with an optional argument to the garden_variety
macro. The
rest of the methods can be overridden as normal, a la carte. For a
detailed description of method behavior, see the
full documentation.
(Note that the authorize
, policy_scope
, and permitted_attributes
methods are provided by Pundit.)
Scaffold generator
garden_variety includes a scaffold generator similar to the Rails scaffold generator:
$ rails generate garden:scaffold post title:string body:text published:boolean
generate resource
invoke active_record
create db/migrate/19991231235959_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/models/post_test.rb
create test/fixtures/posts.yml
invoke controller
create app/controllers/posts_controller.rb
invoke erb
create app/views/posts
invoke test_unit
create test/controllers/posts_controller_test.rb
invoke helper
create app/helpers/posts_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/posts.coffee
invoke scss
create app/assets/stylesheets/posts.scss
invoke resource_route
route resources :posts
generate erb:scaffold
exist app/views/posts
create app/views/posts/index.html.erb
create app/views/posts/edit.html.erb
create app/views/posts/show.html.erb
create app/views/posts/new.html.erb
create app/views/posts/_form.html.erb
insert app/controllers/posts_controller.rb
generate pundit:policy
create app/policies/post_policy.rb
invoke test_unit
create test/policies/post_policy_test.rb
The generated controller will contain only a call to the
garden_variety
macro. Also, as you can see from the command output,
the garden_variety scaffold generator differs from the Rails scaffold
generator in a few small ways:
- No scaffold CSS (i.e. no "app/assets/stylesheets/scaffolds.scss").
- No jbuilder templates. Only HTML templates are generated.
rails generate pundit:policy
is invoked for the specified model.
Beyond garden variety behavior
garden_variety is designed to reduce the amount of custom code written, including in situations where custom code is unavoidable.
Integrating with search
It is possible to integrate searching functionality by overriding the
index
action. However, it can be simpler to override the
list_resources
method instead:
class PostsController < ApplicationController
garden_variety
def list_resources
params[:author] ? super.where(author: params[:author]) : super
end
end
Integrating with pagination
Your favorite pagination gem (may I suggest
foliate?) can also be integrated
by overriding the list_resources
action:
class PostsController < ApplicationController
garden_variety
def list_resources
paginate(super)
end
end
Integrating with authentication
The details of integrating authentication will depend on your chosen authentication library. Devise is the most popular, but Clearance is also a strong choice. Whatever library you choose, it is likely to include a "before filter" which you must invoke in your controller to enforce authentication. Something similar to the following:
class PostsController < ApplicationController
garden_variety
before_action :authenticate_user!
end
Your authentication library is also likely to provide a current_user
method, which will return an appropriate value when the user is
authenticated. Pundit automatically uses this method to enforce
authorization policies. See your authentication library's documentation
to verify that it provides this method, or see Pundit's documentation
for details on using a different method to identify the current user.
garden_variety also provides a stub implementation of current_user
,
so that if no authentication library is chosen, current_user
will be
defined to always return nil.
Note about Clearance: Clearance versions previous to 2.0 define a
deprecated authorize
method which conflicts with Pundit. To avoid
this conflict, add the following line to your Clearance initializer:
::Clearance::Authorization.send(:remove_method, :authorize)
Integrating with Form Objects
The Form Object pattern is used to mitigate the complexity of handling forms which need special processing logic, such as context-dependent validation, or forms which involve more than one model. A detailed explanation of the pattern is beyond the scope of this document, but consider the following minimal example:
class RegistrationForm
include ActiveModel::Model
attr_accessor :email, :password, :accept_terms_of_service
validates :accept_terms_of_service, presence: true, acceptance: true
def save
@user = User.new(email: email, password: password)
if [valid?, @user.valid?].all?
@user.save
else
@user.errors.each{|attr, | errors.add(attr, ) }
false
end
end
end
class RegistrationFormsController < ApplicationController
garden_variety :new
def create
self.resource = vest(new_resource)
if resource.save
redirect_to root_path # redirect to front page instead of show
else
render :new
end
end
end
Only the new
controller action is generated by the garden_variety
macro. The create
action is implemented directly in order to redirect
to root_path
rather than the resource itself, as would be
conventional. The garden_variety helper methods all work as expected
because RegistrationForm
responds to assign_attributes
and save
,
and has a default (nullary) constructor.
This pattern of overriding a controller action merely to respond differently upon success is common enough that garden_variety provides a concise syntax for it:
class RegistrationFormsController < ApplicationController
garden_variety :new, :create
def create
super{ redirect_to root_path }
end
end
In the above example, the garden_variety
macro generates a
conventional create
action, which is then invoked via super
in the
create
override. Here, when a block is passed to super
, it is
treated as an on-success callback which replaces the default redirect.
This callback behavior is also available for the update
and destroy
actions.
Non-REST actions
You may also define any non-REST controller actions you wish (i.e.
actions other than: index
, show
, new
, create
, edit
, update
,
and destroy
). The helper methods garden_variety provides may be
useful when doing so.
However, before implementing a non-REST controller action, consider if
the behavior might be better implemented as a REST action in a new
controller. For example, instead of the following published
action...
class PostsController < ApplicationController
garden_variety
def published
@posts = Post.where(published: true)
render :index
end
end
...consider a new controller:
class PublishedPostsController < ApplicationController
garden_variety :index, resources: :posts
def list_resources
super.where(published: true)
end
end
Note the resources:
argument to the garden_variety
macro. The
resource class for PublishedPostsController
will be overridden as
Post
instead of derived as PublishedPost
. Likewise, the @posts
instance variable will be used instead of @published_posts
.
This example may be somewhat contrived, but there is an excellent talk from RailsConf which delves deeper into the principle: In Relentless Pursuit of REST (slides).
Installation
Add this line to your application's Gemfile:
gem "garden_variety"
Then execute:
$ bundle install
And finally, if you haven't already used and installed Pundit, run the Pundit installation generator:
$ rails generate pundit:install
Contributing
Run rake test
to run the tests.