Posts Tagged generator

Getting a bloody Rails 3 generator running. And testing it.

Wednesday, September 15th, 2010

Suck me sideways! It is said the generator layer in Rails 3 got really ohsom, flexible and whatever. I can now confirm “That’s true, somehow.” – nevertheless it took me years to figure out all the internals, conventions and places where to find documentation.

What I wanted

So here’s my requirement. I have a gem cells. It’s ready for Rails 3. It’s even mentioned in the Rails’ weblog! Anyway, it needs a generator to create assets for the users. In Rails 2.3 running something like

rails2 $ script/generate cell blog latest --haml

would generate several files in app/cells/ inside the rails application.

So, basically, I need a generator inside a gem to create… files. Nothing more.

Where I put it

Here’s how I ended up with my generator. It resides at
cells/lib/generators/cells/cell_generator.rb.

Two things noteworthy here.

  • Put your generators in your gem’s lib/generators directory, nowhere else.
  • The last directory containing the generator class had to be named cells (like the gem). Any other filename worked fine when running rails g, which would list my generator.
    However, when trying to generate with rails g cells:cell it failed with “Could not find generator cells:cell.“.

I was able to find some informations on naming in the generators guide. I don’t really like the example there, and I had to dive into the code to figure out things, anyway.

How I did it

Here’s how the generator looks like (I stole a bit from devise).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
require 'rails/generators'
require 'rails/generators/named_base'
 
module Cells
  module Generators
    class CellGenerator < ::Rails::Generators::NamedBase
      argument :actions, :type => :array, :default => [], :banner => "action action"
      check_class_collision :suffix => "Cell"
 
      source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
 
      class_option :view_engine, :type => :string, :aliases => "-t", :desc => "Template engine for the views. Available options are 'erb' and 'haml'.", :default => "erb"
      class_option :haml, :type => :boolean, :default => false
 
 
      def create_cell_file
        template 'cell.rb', File.join('app/cells', class_path, "#{file_name}_cell.rb")
      end
 
      def create_views
        if options[:view_engine].to_s == "haml" or options[:haml]
          create_views_for(:haml)
        else
          create_views_for(:erb)
        end
      end
 
      def create_test
        @states = actions
        template 'cell_test.rb', File.join('test/cells/', "#{file_name}_cell_test.rb")
      end
 
    protected
 
      def create_views_for(engine)
        for state in actions do
          @state  = state
          @path   = File.join('app/cells', file_name, "#{state}.html.#{engine}")
 
          template "view.#{engine}", @path
        end
      end
    end    
  end
end

Let’s discuss some basics.

  • Derive your generator from Rails::Generators::NamedBase if it expects at least one argument (line 6)
  • Use Generator.source_root to define where your templates are located (line 10)
  • Every public method is invoked automatically during the generation process. That’s something you should know. I didn’t know that.
  • Be sure to read the Thor manual, as this is the base for Rails’ generators. E.g. Generator.template will parse the specified ERB view and drop it in the passed new location (line 17).
  • Options from the command line can be retrieved from the Generator.options method (line 21).

The complete generator files with its views and shit can be found at my repository.

Testing it

I hate manual testing. Instead of calling

rails g cells:cell blog latest --haml

a thousand times I wrote generator tests. No docs on that could be found, so far.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
require File.join(File.dirname(__FILE__), 'test_helper')
 
require 'generators/cells/cell_generator'
 
class CellGeneratorTest < Rails::Generators::TestCase
  destination File.join(Rails.root, "tmp")
  setup :prepare_destination
  tests ::Cells::Generators::CellGenerator
 
  test "create the standard assets" do
    run_generator %w(Blog post latest)
 
    assert_file "app/cells/blog_cell.rb", /class BlogCell < Cell::Rails/
    assert_file "app/cells/blog_cell.rb", /def post/
    assert_file "app/cells/blog_cell.rb", /def latest/
    assert_file "app/cells/blog/post.html.erb", %r(app/cells/blog/post.html.erb)
    assert_file "app/cells/blog/latest.html.erb", %r(app/cells/blog/latest.html.erb)
    assert_file "test/cells/blog_cell_test.rb"
  end

Generator tests are super smooth, just watch out for the following.

  • Be sure to require your generator (line 3).
  • Derive your test from the nice Rails::Generators::TestCase (line 5).
  • With TestCase.run_generator you can simulate a command line invocation of your generator and pass arbitrary arguments (line 11).
  • The handy TestCase.assert_file method checks if the file was generated. Additionally, you can pass a regex as second argument which is matched against the file content.

Props to the very intuitive and easy Generators::TestCase API, I do appreciate that.

Reflecting the past decades

At first, I was overwhelmed by all the features like hooks and namespaces. Now that I understand the principles of Rails 3 generators it almost seems ridiculously easy. And, if you don’t understand things, always remember to peek at the Thor docs. Maybe the generator guide should mention that somewhere.

Peace.