aboutsummaryrefslogblamecommitdiffstats
path: root/railties/lib/rails_generator.rb
blob: 83a404fc0afdd4668d1179e86689fc7302b3af84 (plain) (tree)



















































































































                                                                                                











                                                                         


                                                                                                 










                                                                         


























































                                                                                          
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