Ruby on Rails: Generators
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']