A voting extension from scratch for Rails 3 (Part 5 – Migration Generators)

In the last episode we took a look at the model and schema of the MakeVoteable gem.

However the schema in the spec folder just sets up our required database fields for the testing environment, but does not help us when the gem gets installed in a Rails application. Therefore are the migration files defined in the application itself (which again create the schema file for the application).

One way would be to tell the user in the documentation of the external gem exactly about the migration details the gem needs. As setting up a migration file can be quite time consuming, error prone and boring, there is a much more convenient way for users of your gem … using generators.

There are very different kinds of generators and most likely you have used them for various tasks (creating scaffolds, controllers, and so on). Many of those generators just call other generators again. Here we are interested in migration generators (that by the way are also called when generating scaffolds or models).

Generators should live somewhere inside your gem in a lib/generators folder so that rake finds the generator later in the target application. I usually put it there in some subfolder named after the gem itself (like lib/generators/make_voteable), but if you plan to support multiple database systems it is always a good idea to place it in a subfolder named after the persistence technology used (like lib/generators/active_record).

Below is the pretty simple migration generator of MakeVoteable:

# lib/generators/make_voteable/make_voteable_generator.rb
require 'rails/generators/migration'
require 'rails/generators/active_record'
 
class MakeVoteableGenerator < Rails::Generators::Base
  include Rails::Generators::Migration
 
  desc "Generates a migration for the Vote model"
 
  def self.source_root
    @source_root ||= File.dirname(__FILE__) + '/templates'
  end
 
  def self.next_migration_number(path)
    ActiveRecord::Generators::Base.next_migration_number(path)
  end
 
  def generate_migration
    migration_template 'migration.rb', 'db/migrate/create_make_voteable_tables'
  end
end

All Rails 3 generators extending Rails::Generators::Base are built upon Thor (something similar to rake). In this regard a generator is something like a rake task, but with the characteristic that every public method is automatically executed when running the generator. In the above case the generate_migration method is automatically called which again calls migration_template of the included Rails::Generators::Migration module. As parameter it gets the migration template file (the actual data of the migration) and the location where the migration file should be placed in the target application (usually in the db/migrate folder).

The generate_migration method itself calls the class methods source_root to get the folder location where to look for the migration template. It also calls the class method next_migration_number that generates the migration number, which is prepended to the migration file created in the target application (so create_make_voteable_tables will become something like 20110113003337_create_make_voteable_tables). It is up to the gem developer to provide those callback methods (like in the above generator).

In the MakeVoteable generator I call ActiveRecord::Generators::Base.next_migration_number to let the migration number be generated (that’s why I require explicitly active_record on top of the generator). I am not really sure if this is the recommended way as most other migration generators I saw do it manually (for example like the Resort migration generator), but I found it to be a very convenient way.

To generate the migration in the target application (that has MakeVoteable added to its Gemfile) simply call rails generate make_voteable.

The question is now how to automatically test our generator without setting up a testing app and running the generator manually. In the past this wasn’t that easy with RSpec (used by MakeVoteable), why I have seen some gems testing their generators with TestUnit (even everything else was RSpec) or not testing the generators at all.

Fortunately there is the generator_spec gem by Steve Hodgkiss which makes testing generators with RSpec very easy.

Just add s.add_development_dependency "generator_spec" to your gemspec and add a spec like the MakeVoteable one below:

# spec/generators/make_voteable_generator_spec.rb
require 'spec_helper'
require 'action_controller'
require 'generator_spec/test_case'
require 'generators/make_voteable/make_voteable_generator'
 
describe MakeVoteableGenerator do
  include GeneratorSpec::TestCase
  destination File.expand_path("/tmp", __FILE__)
  tests MakeVoteableGenerator
 
  before do
    prepare_destination
    run_generator
  end
 
  specify do
    puts Dir.new(destination_root).entries
    destination_root.should have_structure {
      directory "db" do
        directory "migrate" do
          migration "create_make_voteable_tables" do
            contains "class CreateMakeVoteableTables"
            contains "create_table :votings"
          end
        end
      end
    }
  end
end

Make sure you require Rails action_controller on top (rails is already a dependency of generator_spec, so you are good to go without adding it to your gemspec).

With destination one has to choose a directory with write access. It is the location where the test migrations are created. In a before block this destination folder is prepared (prepare_destination) and then the generator is run (run_generator) which should create our test migration file.

The specify block now tests if the migration file was created correctly and tests some of its content.

The above migration generator is quite simple. There are more complex scenarios where generators use user defined options, dynamic migration templates using erb style or generators calling other generators.

Here are some more generator resources: