Skip to content
Delightfully boring Rails controllers
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
lib
test
.gitignore
CHANGELOG.md
Gemfile
MIT-LICENSE
README.md
Rakefile
garden_variety.gemspec

README.md

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 relies on the 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
    authorize(self.class.model_class)
    self.collection = policy_scope(find_collection)
  end

  def show
    self.model = authorize(find_model)
  end

  def new
    self.model = authorize(new_model)
    if params.key?(self.class.model_class.model_name.param_key)
      assign_attributes(model)
    end
  end

  def create
    self.model = assign_attributes(authorize(new_model))
    if model.save
      flash[:success] = flash_message(:success)
      redirect_to model
    else
      flash.now[:error] = flash_message(:error)
      render :new
    end
  end

  def edit
    self.model = authorize(find_model)
  end

  def update
    self.model = assign_attributes(authorize(find_model))
    if model.save
      flash[:success] = flash_message(:success)
      redirect_to model
    else
      flash.now[:error] = flash_message(:error)
      render :edit
    end
  end

  def destroy
    self.model = authorize(find_model)
    if model.destroy
      flash[:success] = flash_message(:success)
      redirect_to action: :index
    else
      flash.now[:error] = flash_message(:error)
      render :show
    end
  end

  private

  def self.model_class
    Post
  end

  def collection
    @posts
  end

  def collection=(models)
    @posts = models
  end

  def model
    @post
  end

  def model=(model)
    @post = model
  end

  def find_collection
    self.class.model_class.all
  end

  def find_model
    self.class.model_class.find(params[:id])
  end

  def new_model
    self.class.model_class.new
  end

  def assign_attributes(model)
    model.assign_attributes(permitted_attributes(model))
    model
  end

  def flash_options
    { model_name: "Post" }
  end

  def flash_message(status)
    keys = [
      :"flash.posts.#{action_name}.#{status}",
      :"flash.posts.#{action_name}.#{status}_html",
      :"flash.#{action_name}.#{status}",
      :"flash.#{action_name}.#{status}_html",
      :"flash.#{status}",
      :"flash.#{status}_html",
    ]
    helpers.translate(keys.first, { default: keys.drop(1), **flash_options })
  end

end

The ::model_class method returns a class corresponding to the controller name, by default. That value can be overridden using the matching ::model_class= setter. The model / collection accessor methods are dictated by ::model_class. The rest of the methods can be overridden as normal, a la carte. For a detailed description of method behavior, see the API 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.

Additionally, if you are using the talent_scout gem, the scaffold generator will invoke rails generate talent_scout:search for the specified model. This behavior can be disabled with the --skip-talent-scout option. For more information about integrating with talent_scout, see the Searching with talent_scout section below.

Flash messages

Flash messages are defined using I18n. The garden_variety installer (rails generate garden:install) will create a "config/locales/flash.en.yml" file containing default "success" and "error" messages. You can edit this file to customize those messages, or add your own translation files to support other languages.

As seen in the PostsController#flash_message method in the example above, a prioritized list of keys are tried when retrieving a flash message. Keys specific to the controller are tried first, followed by keys specific to the action, and then finally generic status keys. For each level of specificity, *_html key variants are supported, which allow raw HTML to be included in flash messages and not be escaped when rendered. (See the Safe HTML Translations section of the Rails Internationalization guide for more information.)

Interpolation in flash messages is also supported (as described by Passing Variables to Translations), with interpolation values provided by the flash_options method. By default, flash_options provides a model_name value, but you can override it to provide your own values.

Beyond garden variety behavior

garden_variety is designed to reduce the amount of custom code written, including in situations where custom code is unavoidable.

Eliminating N+1 queries

With proper Russian doll caching, N+1 queries can be a feature. But, if you need to eliminate an N+1 query by using eager loading, you can override the find_collection method:

class PostsController < ApplicationController
  garden_variety

  def find_collection
    super.includes(:author)
  end
end

Pagination

You can integrate your your favorite pagination gem (may I suggest moar?) by overriding the find_collection method:

class PostsController < ApplicationController
  garden_variety

  def find_collection
    moar(super.order(:created_at))
  end
end

Searching

You can provide search functionality by overriding the find_collection method:

class PostsController < ApplicationController
  garden_variety

  def find_collection
    params[:title] ? super.where("title LIKE ?", "%#{params[:title]}%") : super
  end
end

Searching with talent_scout

If you are using the talent_scout gem, the default implementation of find_collection will automatically instantiate your model search class -- no override required. For example, if a PostSearch class is defined, PostsController#find_collection will be equivalent to:

def find_collection
  @search = PostSearch.new(params[:q])
  @search.results
end

Notice, as a side effect, the @search variable is set for later use in the view. The model search class will be chosen based on the controller's ::model_class. For example:

class MyPostsController < ApplicationController
  garden_variety

  self.model_class = Post # find_collection will use PostSearch instead of MyPostSearch
end

If a corresponding model search class is not defined, find_collection will fall back to its original non-search behavior.

Alternatively, you can override the model search class directly:

class MyPostsController < ApplicationController
  garden_variety

  self.model_class = Post
  self.model_search_class = MyPostSearch # find_collection will use MyPostSearch
end

Server-generated JavaScript Responses (SJR)

SJR controller actions are generally the same as conventional controller actions, with one difference: non-GET SJR actions render instead of redirect on success. garden_variety provides a concise syntax for overriding only the on-success behavior of non-GET actions:

class PostsController < ApplicationController
  garden_variety

  def create
    super{ render plain: "text" } # renders text on success instead of redirect
  end
end

Thus, combining this syntax with Rails' default rendering convention:

class PostsController < ApplicationController
  garden_variety

  def create
    respond_to do |format|
      format.js{ super{} } # renders "app/views/posts/create.js.erb" on success
    end
  end

  def update
    respond_to do |format|
      format.js{ super{} } # renders "app/views/posts/update.js.erb" on success
    end
  end

  def destroy
    respond_to do |format|
      format.js{ super{} } # renders "app/views/posts/destroy.js.erb" on success
    end
  end

end

Sidenote: garden_variety automatically manages the life of flash messages. If a redirect is replaced with a render, the flash message will be set such that it does not affect the next request.

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)

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, message| errors.add(attr, message) }
      false
    end
  end
end


class RegistrationFormsController < ApplicationController
  garden_variety :new, :create # only generate #new and #create actions

  def create
    super{ redirect_to root_path } # redirect to home page on success
  end
end

In this example, only the new and create actions are generated by the garden_variety macro. The on-success behavior of create is overridden to redirect to root_path, using the concise syntax discussed in the Integrating with SJR example. The garden_variety controller helper methods all work as expected because RegistrationForm responds to assign_attributes and save, and has a default (nullary) constructor.

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 controller 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
  self.model_class = Post
  garden_variety :index

  def find_collection
    super.where(published: true)
  end
end

Notice the call to ::model_class=. The model class for PublishedPostsController is overridden as Post instead of derived as PublishedPost. And because of this override, the @posts instance variable will be used instead of @published_posts.

This example may be somewhat contrived, but here 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, run the install generator:

$ rails generate garden:install

This will also run the Pundit install generator, if necessary.

Contributing

Run rake test to run the tests.

License

MIT License

You can’t perform that action at this time.