homeASCIIcasts

218: Making Generators in Rails 3 

(view original Railscast)

Other translations: It Es

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.