Rails Generators - What they’re good for and when to use them

August 20th, 2021

Modifying the built in Rails generators or writing your own can help cut down on repetitive work. I’ve found generators most useful in new apps but they can be useful in older code too. This post covers the advantages and disadvantages of generators and provide some tips for when you decide they’re useful to you.

Rails creates every file you ask it for using generators and you have a lot of control over how they work.

You can:

As useful as generators can be they’ll never be able to create everything. Use them to get started and then edit as needed. If you’re creating your own or want to override the defaults try doing the changes by hand a few times first to make sure it’s something you can automate.

For CRUD forms it’s also worth considering a custom FormBuilder subclass. Having your own generator templates plus a custom form builder is a good way of avoiding a large dependency for admin screens.

Generators do take time to write so they’re only worth reaching for if you’re sure you’ll use them, for example twenty models you need the same CRUD scaffolding for. Consider whether it would be faster to just write the code if you’re thinking about writing your own.

Configuring the generators

Here’s a snippet from config/application.rb in an app I recently started.

config.generators do |g|
  g.helper false
  g.stylesheets false
  g.test_framework , false
end

This tells Rails to skip creating helper files, controller specific stylesheets, and the default fixture files.

I find that generated app/helpers mostly stay empty so skipping them saves deleting them later. I don’t use controller specific stylesheets so I don’t want them generated. I do use fixtures but I don’t like the generated defaults so I’d rather create those files myself.

These are pretty specific to how I like to work but if there’s anything you wish Rails did or didn’t do when you run a generator you can change it here.

Overriding the built in generators

I’m using non-scaffold controllers as the example here for smaller code excerpts. The same principles apply if you’re overriding the scaffold controllers and views. (The scaffold generators are in the scaffold_controller directory in railties.)

All the built in Rails generators use .tt as their final file extension. This is so tools that parse Ruby files in your app don’t try and parse the templates. (The .tt stands for Thor Template, Thor is the gem that Rails builds generators on top of.)

Adding devise to controllers

For a web app where every page is behind a login it would be great to have the Devise authenticate_user! line added automatically when generating a new controller. To do this copy the default Rails controller template into your app and change it to add the authenticate_user! line.

Rails keeps the default controller template in the railties gem. In Rails 6.1.3.2 this is lib/rails/generators/rails/controller/templates/controller.rb.tt.

You can copy this to lib/templates in your app but it uses a different path than the gem. To work out where it should go take the gem path from rails onwards without the templates part e.g. rails/controller/templates/controller.rb.tt becomes lib/templates/rails/controller/controller.rb.tt.

mkdir -p lib/templates/rails/controller
cp `bundle info --path railties`/lib/rails/generators/rails/controller/templates/controller.rb.tt lib/templates/rails/controller

Edit the file in lib/templates/rails/controller/controller.rb.tt and add the before_action line:

<% if namespaced? -%>
require_dependency "<%= namespaced_path %>/application_controller"

<% end -%>
<% module_namespacing do -%>
class <%= class_name %>Controller < ApplicationController
<%# ADD THE LINE BELOW TO HAVE DEVISE BY DEFAULT -%>
  before_action :authenticate_user!

<% actions.each do |action| -%>
  def <%= action %>
  end
<%= "\n" unless action == actions.last -%>
<% end -%>
end
<% end -%>

Now when you run bin/rails g controller Thing index it will include the before_action :authorize_user! line.

class ThingController < ApplicationController
  before_action 

  def index
  end
end

Updating the generated tests to include login checks

I use the default Rails testing framework in new apps. At the end of this section there’s some pointers for RSpec users.)

With the code now including a login check the default generated tests will fail because they don’t sign in a user. The template for the tests is not stored in rails/controller/templates to allow different testing frameworks to provide their own templates.

As test unit comes with Rails the templates are in railties again. Copy lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt to lib/templates/test_unit/controller/functional_test.rb.tt, removing the /template/ part of the path as before.

mkdir -p lib/templates/rails/controller
cp `bundle info --path railties`/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt lib/templates/test_unit/controller

The edits are slightly more involved here as the two tests replace the original single test. You might choose to test this differently but whatever way you do it you can update the template to match.

require "test_helper"

<% module_namespacing do -%>
class <%= class_name %>ControllerTest < ActionDispatch::IntegrationTest
<% if mountable_engine? -%>
  include Engine.routes.url_helpers

<% end -%>
<%# ADD LINE HERE TO INCLUDE devise HELPER -%>
  include Devise::Test::IntegrationHelpers

<% if actions.empty? || options[:skip_routes] -%>
  # test "the truth" do
  #   assert true
  # end
<% else -%>
<%# REPLACE THE CONTENTS OF THIS BLOCK WITH YOUR TESTING CODE -%>
<% actions.each do |action| -%>
  test "get <%= action %> works when logged in" do
    sign_in users(:one)
    get <%= url_helper_prefix %>_<%= action %>_url
    assert_response :success
  end

  test "get <%= action %> redirects when not logged in" do
    get <%= url_helper_prefix %>_<%= action %>_url
    assert_response :redirect
  end
<%= "\n" unless action == actions.last -%>
<% end -%>
<% end -%>
end
<% end -%>

This generates:

require "test_helper"

class ThingControllerTest < ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers

  test "get index works when logged in" do
    sign_in users()
    get thing_index_url
    assert_response 
  end

  test "get index redirects when not logged in" do
    get thing_index_url
    assert_response 
  end
end

For RSpec then you’ll want to have a look at the available templates in the rspec-rails gem. There are different types of RSpec controller tests so you’ll need to pick the one you have configured. (The default when I checked was the request_spec.rb template).

rspec-rails doesn’t use the .tt extension. You can add this to your files in lib/templates/rspec if you want to exclude them from anything that looks for Ruby files as Rails will look for .tt versions of any template file it’s asked to find.

Creating a custom generator

The Rails guide covers this well so have a read of that when you’re deciding if you want to build one.

Summary

Rails generators are useful in the right circumstances. Hopefully this post has given you some ideas of how you might use them in your apps and avoid some repetitive work.