Getting a bloody Rails 3 generator running. And testing it.
Wednesday, September 15th, 2010Suck 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/generatorsdirectory, nowhere else. - The last directory containing the generator class had to be named
cells(like the gem). Any other filename worked fine when runningrails g, which would list my generator.
However, when trying to generate withrails g cells:cellit 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::NamedBaseif it expects at least one argument (line 6) - Use
Generator.source_rootto 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.templatewill 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.optionsmethod (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 --hamla 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_generatoryou can simulate a command line invocation of your generator and pass arbitrary arguments (line 11). - The handy
TestCase.assert_filemethod 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.
