Ruby on Rails: Generators

Jelani Woods
5 min readMay 19, 2019

--

Generators can inherit from Rails::Generators::Base. When a generator is invoked, each public method in the generator is executed sequentially in the order that it is defined. Generators can use the some of the same methods that are a part of the Rails Application Templates API. Generators inherit methods like create_file that will create a file at the given destination with the given content.

Creating Generators

Generators themselves have a generator:

rails generate generator initializer
create lib/generators/initializer
create lib/generators/initializer/initializer_generator.rb
create lib/generators/initializer/USAGE
create lib/generators/initializer/templates
invoke test_unit
create test/lib/generators/initializer_generator_test.rb

Notice: we are now inheriting from Rails::Generators::NamedBase instead of Rails::Generators::Base. This means that our generator expects at least one argument, which will be the name of the initializer, and will be available in our code in the variable name.

We can also see that our new generator has a class method called source_root. This method points to where our generator templates will be placed, if any, and by default it points to the created directory lib/generators/initializer/templates.

Templates

To learn about templates, create lib/generators/initializer/templates/initializer.rb with the following content:

class InitializerGenerator < Rails::Generators::NamedBase
source_root File.expand_path('templates', __dir__)

def copy_initializer_file
copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
end
end

Now we can run this generator and pass it a name like, core_extensions

rails generate initializer core_extensions

We can see that now an initializer was created at config/initializers/core_extensions.rb with the contents of our template. The copy_file method copied a file in our source root to the destination path we gave. The method file_name is automatically created when we inherit from Rails::Generators::NamedBase.

Generators with Templates

When you run rails generate initializer core_extensions Rails requires these files in order until one is found:

rails/generators/initializer/initializer_generator.rb
generators/initializer/initializer_generator.rb
rails/generators/initializer_generator.rb
generators/initializer_generator.rb

Customizing All Generators

They can be configured in config/application.rb

config.generators do |g|
g.orm :active_record
g.template_engine :erb
g.test_framework :test_unit, fixture: true
end

The scaffold generator doesn’t actually generate anything, it just invokes other generators that do the work. This allows us to add/replace/remove any of those invocations.

For example, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication.

If we want to avoid generating the default app/assets/stylesheets/scaffolds.scss file when scaffolding a new resource we can disable scaffold_stylesheet:

config.generators do |g|
g.scaffold_stylesheet false
end

The next customization on the workflow will be to stop generating stylesheet, JavaScript and test fixture files for scaffolds altogether. We can achieve that by changing our configuration to the following:

config.generators do |g|
g.orm :active_record
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
g.javascripts false
end

If we generate another resource with the scaffold generator, we can see that stylesheet, JavaScript and fixture files are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it’s just a matter of adding their gems to your application and configuring your generators.

Helper Generator

Let’s create a new helper generator that simply adds some instance variable readers. First, create a generator within the rails namespace, since Rails searches there for generators used as hooks:

rails generate generator rails/my_helper

We’re not going to use source_root or the templates folder.

# lib/generators/rails/my_helper/my_helper_generator.rb
class Rails::MyHelperGenerator < Rails::Generators::NamedBase
def create_helper_file
create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
module #{class_name}Helper
attr_reader :#{plural_name}, :#{plural_name.singularize}
end
FILE
end
end

We can try out our new generator by creating a helper for products:

rails generate my_helper products
create app/helpers/products_helper.rb

And you’ll get a file in app/helpers

module ProductsHelper
attr_reader :products, :product
end

You can even configure the scaffold generator to use that helper

config.generators do |g|
g.orm :active_record
...
g.helper :my_helper
end

Hooks

This works sort of. It doesn’t make the test files for the helper, which maybe we want. We can make a quick change to our helper by providing a hook, then all a test framework needs to implement this hook in order for them to be compatible.

We can update our helper like so

# lib/generators/rails/my_helper/my_helper_generator.rb
class Rails::MyHelperGenerator < Rails::Generators::NamedBase
def create_helper_file
create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
module #{class_name}Helper
attr_reader :#{plural_name}, :#{plural_name.singularize}
end
FILE
end

hook_for :test_framework
end

Now when we call the helper generator and TestUnit is configured as the test framework, Rails will try to invoke Rails::TestUnitGenerator and TestUnit::MyHelperGenerator. These don’t actually exist, but we can make a tiny adjustment to our hook to instead invoke TestUnit::Generators::HelperGenerator, which does exist.

hook_for :test_framework, as: :helper

Customizing Generator Templates

In the previous step we added a line to the helper that allowed tests to be generated for the generator. The better way to do that is to replace the templates of generators that already exist, like Rails::Generators::HelperGenerator.

Generators don’t just look in the source_root for templates. One of the other paths they look in is lib/templates. Since we want to customize Rails::Generators::HelperGenerator , we can do that by making a template copy inside /lib/templates/rails/helper with the name helper.rb. We’ll add the content:

#  lib/templates/rails/helper/helper.rb
module <%= class_name %>Helper
attr_reader :<%= plural_name %>, :<%= plural_name.singularize %>
end

and update our application.rb

config.generators do |g|
g.orm :active_record
...
# g.helper :my_helper
end

And we still are generating our helper! This is useful, since we can customize our scaffold templates and/or layouts by just creating edit.html.erb inside lib.templates/erb/scaffold.

Note: ERB tags need to be escaped with an extra % in templates.

<%%= stylesheet_include_tag :application %>

Generator Fallbacks

Imagine if you wanted to add a feature on top of TestUnit like shoulda does. Since TestUnit already implements all generators required by Rails and shoulda just wants to overwrite part of it. We don’t need to make shoulda reimplement some generators again. It can simply tell Rails to use a TestUnit generator if none was found under the Shoulda namespace.

If we once again modify our application.rb we can make this behavior happen.

config.generators do |g|
g.test_framework :shoulda, fixture: false
...
# Add a fallback!
g.fallbacks[:shoulda] = :test_unit
end

Now creating a scaffold will invoke shoulda generators but fallback to the TestUnit generators.

rails generate scaffold Comment body:text
invoke active_record
create db/migrate/20130924143118_create_comments.rb
create app/models/comment.rb
invoke shoulda
create test/models/comment_test.rb
create test/fixtures/comments.yml
...
invoke test_unit
create test/application_system_test_case.rb
create test/system/comments_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/comments.coffee
invoke scss

Fallbacks allow your generators to have a single responsibility, increasing code reuse and reducing the amount of duplication.

Command Line Arguments

Rails generators can be easily modded to get custom command line arguments. This functionality comes from Thor:

class_option :scope, type: :string, default: 'read_products'

Now our generator can be invoked as follows:

rails generate initializer --scope write_products

The command line arguments are accessed through the options method inside the generator class, like:

@scope = options['scope']

--

--