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.

Tags: , ,

8 Responses to “Getting a bloody Rails 3 generator running. And testing it.”


  1. Stefan

    Hey Nick,
    thanks for sharing this! Saved my day :)

    Cheers


  2. karle

    Hey Nick,

    First off, thanks!!! This also saved my day.

    One of the more important things you also forgot to mention is file naming!

    In the example you give, your file should be called:
    lib/generators/cells/cell_generator.rb

    If, for instance, you tried to rename your generator class to something like “MultiCellGenerator”, your example would no longer work and you would get the error ‘Could not find generator cells:multi_cell’.

    Renaming your file would fix things up, though:
    lib/generators/cells/multi_cell_generator.rb


  3. nick

    @karle: Huh, didn’t I describe that at “Where I put it”? Thanks anyway, maybe the paragraph is a bit confusing…

  4. Thank you very much for sharing this! It’s indeed a life saver.


  5. Bighamster

    Thank you for sharing exp. That helped me to understand principals. Official guide was written without important things to know.

  6. [...] .recentcomments a{display:inline !important;padding:0 !important;margin:0 !important;} 游泳的代码Do one thing, and do it well. Menu Skip to contentHomeAbout 改写Rails 2.x generator到Rails 3.x时的一些记录 Leave a reply google_ad_client = "ca-pub-2238523584906482"; /* blog300x250 */ google_ad_slot = "8001826552"; google_ad_width = 300; google_ad_height = 250; 因为有个基于Rails 2.x的项目用到了feedback插件,在把项目迁移到Rails 3.x版本后,需要让feedback也支持Rails 3.x。但是原作者早已不再更新该插件,所以只能自动动手,花了两天的时间查资料,该代码,终于改写完成,在github上做了个分支。Rails 3.x中生成generator可用下面的命令: rails g generator feedback_form create lib/generators/feedback_form create lib/generators/feedback_form/feedback_form_generator.rb create lib/generators/feedback_form/USAGE create lib/generators/feedback_form/templates class InitializerGenerator < Rails::Generators::NamedBase source_root File.expand_path("../templates", __FILE__) end Rails::Generators::NamedBase和Rails::Generators::Base的区别是NamedBase期望至少一个参数。USAGE是打入–help参数时显示的文档: rails generate feedback_form –help Usage: rails generate feedback_form NAME [options] Rails 2.x的generator中的manifest可以删除了,因为在3.x中每个public方法在生成过程中会被自动调用。参考资料: http://guides.rubyonrails.org/generators.html http://railscasts.com/episodes/218-making-generators-in-rails-3 http://blog.loopedstrange.com/modest-rubyist-archive/rails-3-plugins-part4-more-on-generators http://nicksda.apotomo.de/2010/09/getting-a-bloody-rails-3-generator-running-and-testing-it/ [...]

  7. You actually make it seem so easy with your presentation but I find this
    topic to be actually something that I think I would never understand.
    It seems too complex and extremely broad for me.
    I’m looking forward for your next post, I
    will try to get the hang of it!

    Look at my site: social media marketing expert

Leave a Reply