2004-12-07 06:49:38 -05:00
|
|
|
require 'fileutils'
|
|
|
|
|
|
|
|
module Rails
|
|
|
|
module Generator
|
|
|
|
class GeneratorError < StandardError; end
|
|
|
|
class UsageError < GeneratorError; end
|
|
|
|
|
|
|
|
CONTRIB_ROOT = "#{RAILS_ROOT}/script/generators"
|
|
|
|
BUILTIN_ROOT = "#{File.dirname(__FILE__)}/../generators"
|
|
|
|
DEFAULT_SEARCH_PATHS = [CONTRIB_ROOT, BUILTIN_ROOT]
|
|
|
|
|
|
|
|
class << self
|
|
|
|
def instance(name, args = [], search_paths = DEFAULT_SEARCH_PATHS)
|
|
|
|
# RAILS_ROOT constant must be set.
|
|
|
|
unless Object.const_get(:RAILS_ROOT)
|
|
|
|
raise GeneratorError, "RAILS_ROOT must be set. Did you require 'config/environment'?"
|
|
|
|
end
|
|
|
|
|
|
|
|
# Force canonical name.
|
|
|
|
name = Inflector.underscore(name.downcase)
|
|
|
|
|
|
|
|
# Search for filesystem path to requested generator.
|
|
|
|
unless path = find_generator_path(name, search_paths)
|
|
|
|
raise GeneratorError, "#{name} generator not found."
|
|
|
|
end
|
|
|
|
|
|
|
|
# Check for templates directory.
|
|
|
|
template_root = "#{path}/templates"
|
|
|
|
unless File.directory?(template_root)
|
|
|
|
raise GeneratorError, "missing template directory #{template_root}"
|
|
|
|
end
|
|
|
|
|
|
|
|
# Require class file according to naming convention.
|
|
|
|
require "#{path}/#{name}_generator.rb"
|
|
|
|
|
|
|
|
# Find class according to naming convention. Allow Nesting::In::Modules.
|
|
|
|
class_name = Inflector.classify("#{name}_generator")
|
|
|
|
unless klass = find_generator_class(name)
|
|
|
|
raise GeneratorError, "no #{class_name} class defined in #{path}/#{name}_generator.rb"
|
|
|
|
end
|
|
|
|
|
|
|
|
# Instantiate and return generator.
|
|
|
|
klass.new(template_root, RAILS_ROOT, search_paths, args)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def builtin_generators
|
|
|
|
generators([BUILTIN_ROOT])
|
|
|
|
end
|
|
|
|
|
|
|
|
def contrib_generators
|
|
|
|
generators([CONTRIB_ROOT])
|
|
|
|
end
|
|
|
|
|
|
|
|
def generators(search_paths)
|
|
|
|
generator_paths(search_paths).keys.uniq.sort
|
|
|
|
end
|
|
|
|
|
|
|
|
# Find all generator paths.
|
|
|
|
def generator_paths(search_paths)
|
|
|
|
@paths ||= {}
|
|
|
|
unless @paths[search_paths]
|
|
|
|
paths = Hash.new { |h,k| h[k] = [] }
|
|
|
|
search_paths.each do |path|
|
|
|
|
Dir["#{path}/[a-z]*"].each do |dir|
|
|
|
|
paths[File.basename(dir)] << dir if File.directory?(dir)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
@paths[search_paths] = paths
|
|
|
|
end
|
|
|
|
@paths[search_paths]
|
|
|
|
end
|
|
|
|
|
|
|
|
def find_generator_path(name, search_paths)
|
|
|
|
generator_paths(search_paths)[name].first
|
|
|
|
end
|
|
|
|
|
|
|
|
# Find all generator classes.
|
|
|
|
def generator_classes
|
|
|
|
classes = Hash.new { |h,k| h[k] = [] }
|
|
|
|
class_re = /([^:]+)Generator$/
|
|
|
|
ObjectSpace.each_object(Class) do |object|
|
|
|
|
if md = class_re.match(object.name) and object < Rails::Generator::Base
|
|
|
|
classes[Inflector.underscore(md.captures.first)] << object
|
|
|
|
end
|
|
|
|
end
|
|
|
|
classes
|
|
|
|
end
|
|
|
|
|
|
|
|
def find_generator_class(name)
|
|
|
|
generator_classes[name].first
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Talk about generators.
|
|
|
|
class Base
|
|
|
|
attr_reader :template_root, :destination_root, :args, :options,
|
|
|
|
:class_name, :singular_name, :plural_name
|
|
|
|
|
|
|
|
alias_method :file_name, :singular_name
|
|
|
|
alias_method :table_name, :plural_name
|
|
|
|
|
|
|
|
def self.generator_name
|
|
|
|
Inflector.underscore(name.gsub('Generator', ''))
|
|
|
|
end
|
|
|
|
|
|
|
|
def initialize(template_root, destination_root, search_paths, args)
|
|
|
|
@template_root, @destination_root = template_root, destination_root
|
|
|
|
usage if args.empty?
|
|
|
|
@search_paths, @original_args = search_paths, args.dup
|
|
|
|
@class_name, @singular_name, @plural_name = inflect_names(args.shift)
|
|
|
|
@options = extract_options!(args)
|
|
|
|
@args = args
|
|
|
|
end
|
|
|
|
|
2004-12-22 19:52:34 -05:00
|
|
|
# Checks whether the class name that was assigned to this generator
|
|
|
|
# would cause a collision with a Class, Module or other constant
|
|
|
|
# that is already used up by Ruby or RubyOnRails.
|
|
|
|
def collision_with_builtin?
|
|
|
|
builtin = Object.const_get(full_class_name) rescue nil
|
|
|
|
type = case builtin
|
|
|
|
when Class: "Class"
|
|
|
|
when Module: "Module"
|
|
|
|
else "Constant"
|
|
|
|
end
|
|
|
|
|
|
|
|
if builtin then
|
2004-12-22 20:08:51 -05:00
|
|
|
"Sorry, you can't have a #{self.class.generator_name} named " +
|
|
|
|
"'#{full_class_name}' because Ruby or Rails already has a #{type} with that name.\n" +
|
|
|
|
"Please rerun the generator with a different name."
|
2004-12-22 19:52:34 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Returns the complete name that the resulting Class would have.
|
|
|
|
# Used in collision_with_builtin(). The default guess is that it is
|
|
|
|
# the same as class_name. Override this in your generator in case
|
|
|
|
# it is wrong.
|
|
|
|
def full_class_name
|
|
|
|
class_name
|
|
|
|
end
|
|
|
|
|
2004-12-07 06:49:38 -05:00
|
|
|
protected
|
|
|
|
# Look up another generator with the same arguments.
|
|
|
|
def generator(name)
|
|
|
|
Rails::Generator.instance(name, @original_args, @search_paths)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Generate a file for a Rails application using an ERuby template.
|
|
|
|
# Looks up and evalutes a template by name and writes the result
|
|
|
|
# to a file relative to +destination_root+. The template
|
|
|
|
# is evaluated in the context of the optional eval_binding argument.
|
|
|
|
#
|
|
|
|
# The ERB template uses explicit trim mode to best control the
|
|
|
|
# proliferation of whitespace in generated code. <%- trims leading
|
|
|
|
# whitespace; -%> trims trailing whitespace including one newline.
|
|
|
|
def template(template_name, destination_path, eval_binding = nil)
|
|
|
|
# Determine full paths for source and destination files.
|
|
|
|
template_path = find_template_path(template_name)
|
|
|
|
destination_path = File.join(destination_root, destination_path)
|
|
|
|
|
|
|
|
# Create destination directories.
|
|
|
|
FileUtils.mkdir_p(File.dirname(destination_path))
|
|
|
|
|
|
|
|
# Render template and write result.
|
|
|
|
eval_binding ||= binding
|
|
|
|
contents = ERB.new(File.read(template_path), nil, '-').result(eval_binding)
|
|
|
|
File.open(destination_path, 'w') { |file| file.write(contents) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def usage
|
|
|
|
raise UsageError.new, File.read(usage_path)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
def find_template_path(template_name)
|
|
|
|
name, path = template_name.split('/', 2)
|
|
|
|
if path.nil?
|
|
|
|
File.join(template_root, name)
|
|
|
|
elsif generator_path = Rails::Generator.find_generator_path(name, @search_paths)
|
|
|
|
File.join(generator_path, 'templates', path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def inflect_names(name)
|
|
|
|
camel = Inflector.camelize(Inflector.underscore(name))
|
|
|
|
under = Inflector.underscore(camel)
|
|
|
|
plural = Inflector.pluralize(under)
|
|
|
|
[camel, under, plural]
|
|
|
|
end
|
|
|
|
|
|
|
|
def extract_options!(args)
|
|
|
|
if args.last.is_a?(Hash) then args.pop else {} end
|
|
|
|
end
|
|
|
|
|
|
|
|
def usage_path
|
|
|
|
"#{template_root}/../USAGE"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|