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 # 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 "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." 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 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