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 runningrails g
, which would list my generator.
However, when trying to generate withrails 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).
require 'rails/generators' require 'rails/generators/named_base' module Cells module Generators class CellGenerator :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.
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.
Hey Nick,
thanks for sharing this! Saved my day 🙂
Cheers
LikeLike
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
LikeLike
@karle: Huh, didn’t I describe that at “Where I put it”? Thanks anyway, maybe the paragraph is a bit confusing…
LikeLike
Thank you very much for sharing this! It’s indeed a life saver.
LikeLike
Thank you for sharing exp. That helped me to understand principals. Official guide was written without important things to know.
LikeLike