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:
- Stop Rails creating empty
foo_helper.rb
files. - Add authorisation to generated controllers.
- Configure CRUD scaffold views for Bootstrap or Tailwind so you can build them quicker.
- Wrap generated text in templates with
t(...)
calls so they’re i18n’d from the start. - If you use patterns like commands or presenters you can create your own generators to write the boilerplate for you.
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.
.generators do |g|
config.helper false
g.stylesheets false
g.test_framework :test_unit, fixture: false
gend
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
:authorize_user!
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
:one)
sign_in users(
get thing_index_url:success
assert_response end
test "get index redirects when not logged in" do
get thing_index_url:redirect
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.