Rails generators dissection
From WhyNotWiki
[edit] How it finds which migration to run
As our example, let's examine:
> ./script/generate migration create_mice
Now we'll dissect the Rails source and figure out how it finds the "migration" generator.
/usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator
generators/ scripts/ base.rb commands.rb lookup.rb manifest.rb options.rb scripts.rb simple_logger.rb spec.rb
/usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/scripts.rb
# Generator scripts handle command-line invocation. Each script
# responds to an invoke! class method which handles option parsing
# and generator invocation.
class Base
include Options
default_options :collision => :ask, :quiet => false
# Run the generator script. Takes an array of unparsed arguments
# and a hash of parsed arguments, takes the generator as an option
# or first remaining argument, and invokes the requested command.
def run(args = [], runtime_options = {})
parse!(args.dup, runtime_options)
# Generator name is the only required option.
unless options[:generator]
usage if args.empty?
options[:generator] ||= args.shift
end
p options #
# Look up generator instance and invoke command on it.
Rails::Generator::Base.instance(options[:generator], args, options).command(options[:command]).invoke!
> ./script/generate migration f
{:command=>:create, :generator=>"migration"}
/usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/lookup.rb
# Generator lookup is managed by a list of sources which return specs
# describing where to find and how to create generators. This module
# provides class methods for manipulating the source list and looking up
# generator specs, and an #instance wrapper for quickly instantiating
# generators by name.
#
# A spec is not a generator: it's a description of where to find
# the generator and how to create it. A source is anything that
# yields generators from #each. PathSource and GemSource are provided.
module Lookup
def self.append_features(base)
super
base.extend(ClassMethods)
base.use_component_sources!
end
# Convenience method to instantiate another generator.
def instance(generator_name, args, runtime_options = {})
self.class.instance(generator_name, args, runtime_options)
end
...
# Convenience method to lookup and instantiate a generator.
def instance(generator_name, args = [], runtime_options = {})
p args
p full_options(runtime_options)
lookup(generator_name).klass.new(args, full_options(runtime_options))
end
> ./script/generate migration create_mice
["create_mice"]
{:command=>:create, :generator=>"migration", :quiet=>false, :collision=>:ask}
/usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/lookup.rb
# Use component generators (model, controller, etc).
# 1. Rails application. If RAILS_ROOT is defined we know we're
# generating in the context of a Rails application, so search
# RAILS_ROOT/generators.
# 2. User home directory. Search ~/.rails/generators.
# 3. RubyGems. Search for gems named *_generator.
# 4. Builtins. Model, controller, mailer, scaffold.
def use_component_sources!
reset_sources
if defined? ::RAILS_ROOT
sources << PathSource.new(:lib, "#{::RAILS_ROOT}/lib/generators")
sources << PathSource.new(:vendor, "#{::RAILS_ROOT}/vendor/generators")
sources << PathSource.new(:plugins, "#{::RAILS_ROOT}/vendor/plugins/**/generators")
end
sources << PathSource.new(:user, "#{Dir.user_home}/.rails/generators")
sources << GemSource.new if Object.const_defined?(:Gem)
sources << PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/components")
end
...
# Lookup and cache every generator from the source list.
def cache
@cache ||= sources.inject([]) { |cache, source| cache + source.map }
end
...
# Lookup knows how to find generators' Specs from a list of Sources.
# Searches the sources, in order, for the first matching name.
def lookup(generator_name)
@found ||= {}
generator_name = generator_name.to_s.downcase
puts cache.class.name
@found[generator_name] ||= cache.find { |spec| spec.name == generator_name }
unless @found[generator_name]
chars = generator_name.scan(/./).map{|c|"#{c}.*?"}
rx = /^#{chars}$/
gns = cache.select{|spec| spec.name =~ rx }
@found[generator_name] ||= gns.first if gns.length == 1
raise GeneratorError, "Pattern '#{generator_name}' matches more than one generator: #{gns.map{|sp|sp.name}.join(', ')}" if gns.length > 1
end
@found[generator_name] or raise GeneratorError, "Couldn't find '#{generator_name}' generator"
end
/usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/lookup.rb
# PathSource looks for generators in a filesystem directory.
class PathSource < Source
attr_reader :path
def initialize(label, path)
super label
@path = path
end
# Yield each eligible subdirectory.
def each
Dir["#{path}/[a-z]*"].each do |dir|
if File.directory?(dir)
puts dir
yield Spec.new(File.basename(dir), dir, label)
end
end
end
end
> ./script/generate migration create_mice script/../config/../vendor/plugins/surveys/generators/create_surveys_table_migration ... script/../config/../vendor/plugins/helper_test/generators/helper_test /usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/generators/components/migration /usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/generators/components/plugin ... /usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/generators/components/model /usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/generators/components/mailer /usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/generators/components/controller
So the generator it will use is /usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/generators/components/migration .
[edit] How to override a built-in generator: Example: Override migration generator to get the latest from Subversion
Fortunately for us, Rails looks in a couple of local/overridable locations (such as PathSource.new(:lib, "#{::RAILS_ROOT}/lib/generators")) before it ever goes searching for its built-in generators (PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/components")).
So we can just copy /usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/generators/components/migration/migration_generator.rb over to lib/generators/migration/migration_generator.rb and it will use lib/generators/migration/migration_generator.rb instead of the other.
Now let's suppose we want to add some code to make sure we have the latest migrations from Subversion before generating a migration. This is to ensure that if someone else has committed, say, migration 18, that we will be aware of that and create migration 19 rather than creating another 18!
This is what we start with:
class MigrationGenerator < Rails::Generator::NamedBase
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
end
end
end
We can continue to use the migration_template command, which is defined here: /usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/commands.rb
# When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template.
def migration_template(relative_source, relative_destination, template_options = {})
migration_directory relative_destination
migration_file_name = template_options[:migration_file_name] || file_name
raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists?(migration
template(relative_source, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options)
end
Where does file_name come from?
/usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/base.rb
attr_reader :name, :class_name, :singular_name, :plural_name, :table_name
alias_method :file_name, :singular_name
def assign_names!(name)
@name = name
base_name, @class_path, @file_path, @class_nesting, @class_nesting_depth = extract_modules(@name)
@class_name_without_nesting, @singular_name, @plural_name = inflect_names(base_name)
puts name, base_name, self.file_name
@table_name = ActiveRecord::Base.pluralize_table_names ? plural_name : singular_name
if @class_nesting.empty?
@class_name = @class_name_without_nesting
else
@class_name = "#{@class_nesting}::#{@class_name_without_nesting}"
end
end
> ./script/generate migration CreateMice CreateMice CreateMice create_mice > ./script/generate migration Snowscape::CreateIcicles Snowscape::CreateIcicles CreateIcicles create_icicles > ./script/generate migration barnyard/create_mice barnyard/create_mice create_mice create_mice
A first attempt...
class MigrationGenerator < Rails::Generator::NamedBase
def initialize(runtime_args, runtime_options = {})
sh 'svn up db/migrations' # Get everyone else's latest revisions so that we can correctly determine the next migration number
super
end
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
new_file = Dir["db/migrate/*#{self.file_name}*"].first
puts "You just created '#{new_file}'"
# We would svn add the file now, but it doesn't really exist yet...
end
end
end
But since we're only creating the manifest right now, not actually running these commands yet (they will be run by the Commands::Create class after the manifest is done being created), the file doesn't exist yet!
> ./script/generate migration create_mice
svn up db/migrations
At revision 1546.
You just created ''
exists db/migrate
create db/migrate/018_create_mice.rb
So... Looks like we'll have to create a custom command. Fortunately, that is easy enough.
This works:
lib/generators/migration/migration_generator.rb
module Rails
module Generator
module Commands
class Create < Base
def add_latest_migration_to_svn(relative_destination)
new_file = Dir["db/migrate/*#{self.file_name}*"].first
migration_number = new_file.match(/[0-9]+/)[0]
puts "Your new migration will now be added to the repository immediately so that other people won't accidentally create a migration with the same n
sh "svn add #{new_file}"
sh "svn ci -m 'Added new migration \"#{new_file}\"' #{new_file}"
end
end
end
end
end
class MigrationGenerator < Rails::Generator::NamedBase
def initialize(runtime_args, runtime_options = {})
sh 'svn up db/migrations' # Get everyone else's latest revisions so that we can correctly determine the next migration number
super
end
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
m.add_latest_migration_to_svn 'db/migrate'
end
end
end
[edit] How script/destroy works
No, they didn't have to make a separate destroy script for each generator. What they appear to do instead is to just run the generator's manifest in reverse!
/usr/lib/ruby/gems/1.8/gems/rails-1.1.6/lib/rails_generator/commands.rb
# Undo the actions performed by a generator. Rewind the action
# manifest and attempt to completely erase the results of each action.
class Destroy < RewindBase
# Remove a file if it exists and is a file.
def file(relative_source, relative_destination, file_options = {})
destination = destination_path(relative_destination)
if File.exists?(destination)
logger.rm relative_destination
unless options[:pretend]
if options[:svn]
# If the file has been marked to be added
# but has not yet been checked in, revert and delete
if options[:svn][relative_destination]
system("svn revert #{destination}")
FileUtils.rm(destination)
else
# If the directory is not in the status list, it
# has no modifications so we can simply remove it
system("svn rm #{destination}")
end
else
FileUtils.rm(destination)
end
end
else
logger.missing relative_destination
return
end
end
