218: Making Generators in Rails 3
(view original Railscast)
As we showed in Episode 216 [watch, read] the generators in Rails 3 are much more modular and can be customized in ways that were never possible with Rails 2. There are still times, though, when you might want to build a brand-new generator from scratch because the built-in ones don’t fit your needs. Making a custom generator was discussed back in Episode 58 but the technique has changed substantially since then so we’re going to revise that episode and show how to create the generator from that episode in Rails 3.
Getting Started
The easiest way to create a generator is from inside a new Rails 3 application, so we’ll start by creating one. Rails 3.0 beta 4 has just been released and the syntax for creating a new application has changed, so to create the todo application we’re going to use to write our generator instead of running
$ rails todo
we need to run
$ rails new todo
Once we’ve created our new application we can begin to create our generator by using one of the built-in generators. The rails g generator
command will create the basic files we need for our new generator. We can examine the parameters that the command takes by running
$ rails g generator --help
The generator is fairly straightforward and just needs to be passed the name of the generator we want to create. We want our generator to generate an application layout file so we’ll call it layout
.
$ rails g generator layout create lib/generators/layout create lib/generators/layout/layout_generator.rb create lib/generators/layout/USAGE create lib/generators/layout/templates
The generator creates files under our application’s /lib
directory which means that the generator will only be available to this application. To make it available to any application we’ll have to do some extra work and we’ll cover that later in this episode.
The generator file that is created is fairly straightforward and just contains a class. In the class is a line of code that tells the generator to look in the templates directory for any additional files.
/lib/generators/layout/layout_generator.rb
class LayoutGenerator < Rails::Generators::NamedBase source_root File.expand_path('../templates', __FILE__) end
The generator
generator also creates a USAGE
file which is used to define the documentation for our generator. This documentation will be shown when we run the --help
option for the generator.
The class created by the generator inherits from Rails::Generators::NamedBase
. This means that a name will be required when running the generator in the same way as we had to provide a name when we ran the generator
generator. We can see this by running our new generator with the --help
option.
$ rails g layout --help Usage: rails generate layout NAME [options] Runtime options: -f, [--force] # Overwrite files that already exist -p, [--pretend] # Run but do not make any changes -s, [--skip] # Skip files that already exist -q, [--quiet] # Supress status output Description: Explain the generator Example: rails generate layout Thing This will create: what/will/it/create
Note that the contents of the USAGE
file are shown at the bottom of the output.
We want the NAME
option to be optional and to have a default of application
. We can do this by having the class inherit from Rails::Generators::Base
, instead of NamedBase
. This will make all of the arguments optional and therefore give us more flexibility to customize the generator to our needs.
We can define the arguments that the generator takes by using the argument
method. Defining our own arguments instead of using the default NAME
option means that we can define a default value for each argument. We’ll add a layout_name
argument with a default value of application
.
/lib/generators/layout/layout_generator.rb
class LayoutGenerator < Rails::Generators::Base source_root File.expand_path('../templates', __FILE__) argument :layout_name, :type => :string, :default => "application" end
If we run the generator command with the --help
option again we’ll see our new argument listed. It is shown in square brackets meaning that it is optional.
$ rails g layout --help Usage: rails generate layout [LAYOUT_NAME] [options]
The generators in Rails 3 are build upon the Thor library, which is similar to rake. A lot of the methods that we’ll call in our generator class are methods that are defined in Thor itself so if you want to know more about what these methods do then it’s well worth taking a look a the Thor source code and documentation.
Creating The Files the Generator Will Use
We want our generator create two files: an application layout file and a stylesheet file. To do this we’ll create two files into the generator’s templates
directory and have the generator copy these files to the correct directories in our application when the generator is run.
So how do we define the generator’s behaviour? Well, the way this is done is rather unusual. Any public method defined in the generator’s class will be executed when the generator is run. This means we can define a method called generate_layout
and it will be automatically executed when we run the generator. This is an unusual concept at first but it makes for a good way to organize the code in the generator.
The first thing we’ll get our generator to do is copy a stylesheet from the templates
directory to our application’s /public/stylesheets
directory and we can use the copy_file
method to do this.
/lib/generators/layout/layout_generator.rb
class LayoutGenerator < Rails::Generators::Base source_root File.expand_path('../templates', __FILE__) argument :layout_name, :type => :string, :default => "application" def generate_layout copy_file "stylesheet.css", "public/stylesheets/#{layout_name.underscore}.css" end end
The copy_file
method takes two arguments. The first is the name of the file in the templates
directory that will be copied and the second is the destination path. Note that the destination file name is based on the layout_name
argument that is passed to the generator. The argument
method creates a layout_name
method we can use, but as the layout name passed in might be camel-cased we’ll call underscore
on it to make sure that it’s in a format that’s applicable for file names.
Finally we’ll need to create the stylesheet.css
file in the templates
directory and paste in the we want for our applications.
If we run our generator now without any arguments it should create an application.css
file.
$ rails g layout
create public/stylesheets/application.css
Next we’ll need to do the same for the layout file. We’ll be using the underscored version of the layout name a few times so it will be best to extract that out into a method that we’ll call file_name
. As we mentioned earlier, any public method in the generator will be executed so we’ll need to make the file_name
method private.
The next thing we need to do inside our generator is to create the layout file itself. We could use copy_file
again but that would just do a straight copy of the template file and we want to run some erb code in the template file so that it is modified when the generator runs.
To do this we can use a template
method, which takes similar arguments to copy_file
but which will parse any erb in the template before copying it to the destination directory.
/lib/generators/layout/layout_generator.rb
class LayoutGenerator < Rails::Generators::Base source_root File.expand_path('../templates', __FILE__) argument :layout_name, :type => :string, :default => "application" def generate_layout copy_file "stylesheet.css", "public/stylesheets/#{file_name}.css" template "layout.html.erb", "app/views/layouts/#{file_name}.html.erb" end private def file_name layout_name.underscore end end
Next we’ll create the template file itself in the templates
directory. The code in the template file will look like this:
/lib/generators/layout/templates/layout.html.erb
<!DOCTYPE html> <html> <head> <title>Untitled</title> <%%= stylesheet_link_tag "<%= file_name %>" %> <%%= javascript_include_tag :defaults %> <%%= csrf_meta_tag %> <%%= yield(:head) %> </head> <body> <div id="container"> <%% flash.each do |name, msg| %> <%%= content_tag :div, msg, :id => "flash_#{name}" %> <%% end %> <%%= yield %> </div> </body> </html>
The first thing to note is that because we’re using the template
method, all of the erb tags in the code will be executed when the generator runs. If we want to include any erb in the generated file we’ll have to escape the percent sign at the beginning of each erb tag and we’ve done that for most of the erb code above.
To include dynamic content in template we can include normal erb code. We can access methods in the generator file, even private methods, and so we can call file_name
to put the correct name for the stylesheet file into the call to stylesheet_link_tag
.
To see if it’s all working we’ll run our generator again and this time pass in a layout name.
$ rails g layout admin create public/stylesheets/admin.css create app/views/layouts/admin.html.erb
This time the generator has created two files, each with a name based on the layout name we passed in, so everything seems to be working. If we look in the generated layout file we’ll see that the file_name
method was called when the file was generated and so the stylesheet_link_tag
method has the correct argument. The other erb tags were escaped and so appear in the layout.
/app/views/layouts/admin.html.erb
<head> <title>Untitled</title> <%= stylesheet_link_tag "admin" %> <%= javascript_include_tag :defaults %> <%= csrf_meta_tag %> <%= yield(:head) %> </head>
Making Options Optional
There’s one more small feature we want to add to our generator: the ability to pass an option that will stop the stylesheet file being generated. We can do this by using the class_option
method.
/lib/generators/layout/layout_generator.rb
class LayoutGenerator < Rails::Generators::Base source_root File.expand_path('../templates', __FILE__) argument :layout_name, :type => :string, :default => "application" class_option :stylesheet, :type => :boolean, :default => true, :description => "Include stylesheet file" def generate_layout copy_file "stylesheet.css", "public/stylesheets/#{file_name}.css" if options.stylesheet? template "layout.html.erb", "app/views/layouts/#{file_name}.html.erb" end private def file_name layout_name.underscore end end
We’ve called our option stylesheet
, given it a type of boolean
with a default value of true
and supplied a description. In the copy_file
line we can now add an if
statement so that the stylesheet file is only copied if the stylesheet
option is true
.
Similarly in the layout template file we only want to include the stylesheet_link_tag
line if the stylesheet
option is true
. We’ll wrap that line in an if
statement so that this happens.
/lib/generators/layout/templates/layout.html.erb
<!DOCTYPE html> <html> <head> <title>Untitled</title> <%- if options.stylesheet? -%> <%%= stylesheet_link_tag "<%= file_name %>" %> <%- end -%> <%%= javascript_include_tag :defaults %> <%%= csrf_meta_tag %> <%%= yield(:head) %> </head> <body> <div id="container"> <%% flash.each do |name, msg| %> <%%= content_tag :div, msg, :id => "flash_#{name}" %> <%% end %> <%%= yield %> </div> </body> </html>
If we run the help documentation for our generator again now we’ll see that we have a stylesheet option.
$ rails g layout --help Usage: rails generate layout [LAYOUT_NAME] [options] Options: [--stylesheet] # Indicates when to generate stylesheet # Default: true
We can now generate a layout without a stylesheet by using the --skip-stylesheet
(or --no-stylesheet
) option. If we do this then only the layout file will be created.
$ rails g layout foo --skip-stylesheet
create app/views/layouts/foo.html.erb
That’s it for this episode on making generators in Rails 3. You’re encouraged to give it a try. Anywhere where you’re generating the same (or similar) code in your rails applications can be converted into a generator. To share your generator with others all you have to do is create a Rails plugin or gem and place your generator in a lib/generators
directory. Then when anyone includes that gem or plugin in their application the generator will be available for them to use.