aboutsummaryrefslogblamecommitdiffstats
path: root/railties/lib/rails/generators.rb
blob: 67037106e513a11f74894bfcc222b3e395ef6b6a (plain) (tree)
1
2
3
4
5
6
                                                                                

                                                                                                         
                    
                       
 






                                                            
 

                   

                                    






                                                                  
 

                             
                       
              










                                   

        
                  
                                  
       
     
 
                       
              
                   






                               




                                                  
                                  

                              
       
     
 
                                        
                                  


                                              
                                       
                                            
                          
                                                

       
                                    
                            

       
                             
                                      

       
                             
                                      

       











                                                                                 
                      
                       

       

                                                                            
                                                                     
                                                       

                                                     

                             
                  
                      
                      
                            
       
 
                               

                                                     

       
                                   


                                           
 




                                                                                  




                                                    
                                                      


                  
                           

                             




                                

                             
                        
                                   
                                 




                               










                                            
                                                  
                                       
                                                               

                             
                                                                     


                                                                    
                                                        

                                             

          


                      
                              
             
                                 
       
 


                                                    
 


                                    
 
                                            
                                    
                                         

                                 
 
                                    
                                             
                         
                            
 
                                                          
 
                                             

       
















                                                                                    
 



                                      
           
                            

         



                                                                            
 


                                     
 

                                                                             
 


                                                                               
                                                        

















                                                                                                                              

         
                                         
                                                        
                                                        












                                                        

                                     

         

                                                           

         

                                                                                        
         
     
   
activesupport_path = File.expand_path("../../../../activesupport/lib", __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)

require "thor/group"
require "rails/command"

require "active_support"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/hash/deep_merge"
require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/string/inflections"

module Rails
  module Generators
    include Rails::Command::Behavior

    autoload :Actions,         "rails/generators/actions"
    autoload :ActiveModel,     "rails/generators/active_model"
    autoload :Base,            "rails/generators/base"
    autoload :Migration,       "rails/generators/migration"
    autoload :NamedBase,       "rails/generators/named_base"
    autoload :ResourceHelpers, "rails/generators/resource_helpers"
    autoload :TestCase,        "rails/generators/test_case"

    mattr_accessor :namespace

    DEFAULT_ALIASES = {
      rails: {
        actions: "-a",
        orm: "-o",
        javascripts: "-j",
        javascript_engine: "-je",
        resource_controller: "-c",
        scaffold_controller: "-c",
        stylesheets: "-y",
        stylesheet_engine: "-se",
        scaffold_stylesheet: "-ss",
        template_engine: "-e",
        test_framework: "-t"
      },

      test_unit: {
        fixture_replacement: "-r",
      }
    }

    DEFAULT_OPTIONS = {
      rails: {
        api: false,
        assets: true,
        force_plural: false,
        helper: true,
        integration_tool: nil,
        javascripts: true,
        javascript_engine: :js,
        orm: false,
        resource_controller: :controller,
        resource_route: true,
        scaffold_controller: :scaffold_controller,
        stylesheets: true,
        stylesheet_engine: :css,
        scaffold_stylesheet: true,
        test_framework: false,
        template_engine: :erb
      }
    }

    def self.configure!(config) #:nodoc:
      api_only! if config.api_only
      no_color! unless config.colorize_logging
      aliases.deep_merge! config.aliases
      options.deep_merge! config.options
      fallbacks.merge! config.fallbacks
      templates_path.concat config.templates
      templates_path.uniq!
      hide_namespaces(*config.hidden_namespaces)
    end

    def self.templates_path #:nodoc:
      @templates_path ||= []
    end

    def self.aliases #:nodoc:
      @aliases ||= DEFAULT_ALIASES.dup
    end

    def self.options #:nodoc:
      @options ||= DEFAULT_OPTIONS.dup
    end

    # Hold configured generators fallbacks. If a plugin developer wants a
    # generator group to fallback to another group in case of missing generators,
    # they can add a fallback.
    #
    # For example, shoulda is considered a test_framework and is an extension
    # of test_unit. However, most part of shoulda generators are similar to
    # test_unit ones.
    #
    # Shoulda then can tell generators to search for test_unit generators when
    # some of them are not available by adding a fallback:
    #
    #   Rails::Generators.fallbacks[:shoulda] = :test_unit
    def self.fallbacks
      @fallbacks ||= {}
    end

    # Configure generators for API only applications. It basically hides
    # everything that is usually browser related, such as assets and session
    # migration generators, and completely disable helpers and assets
    # so generators such as scaffold won't create them.
    def self.api_only!
      hide_namespaces "assets", "helper", "css", "js"

      options[:rails].merge!(
        api: true,
        assets: false,
        helper: false,
        template_engine: nil
      )

      if ARGV.first == "mailer"
        options[:rails].merge!(template_engine: :erb)
      end
    end

    # Remove the color from output.
    def self.no_color!
      Thor::Base.shell = Thor::Shell::Basic
    end

    # Returns an array of generator namespaces that are hidden.
    # Generator namespaces may be hidden for a variety of reasons.
    # Some are aliased such as "rails:migration" and can be
    # invoked with the shorter "migration", others are private to other generators
    # such as "css:scaffold".
    def self.hidden_namespaces
      @hidden_namespaces ||= begin
        orm      = options[:rails][:orm]
        test     = options[:rails][:test_framework]
        template = options[:rails][:template_engine]
        css      = options[:rails][:stylesheet_engine]

        [
          "rails",
          "resource_route",
          "#{orm}:migration",
          "#{orm}:model",
          "#{test}:controller",
          "#{test}:helper",
          "#{test}:integration",
          "#{test}:mailer",
          "#{test}:model",
          "#{test}:scaffold",
          "#{test}:view",
          "#{test}:job",
          "#{template}:controller",
          "#{template}:scaffold",
          "#{template}:mailer",
          "#{css}:scaffold",
          "#{css}:assets",
          "css:assets",
          "css:scaffold"
        ]
      end
    end

    class << self
      def hide_namespaces(*namespaces)
        hidden_namespaces.concat(namespaces)
      end
      alias hide_namespace hide_namespaces
    end

    # Show help message with available generators.
    def self.help(command = "generate")
      puts "Usage: rails #{command} GENERATOR [args] [options]"
      puts
      puts "General options:"
      puts "  -h, [--help]     # Print generator's options and usage"
      puts "  -p, [--pretend]  # Run but do not make any changes"
      puts "  -f, [--force]    # Overwrite files that already exist"
      puts "  -s, [--skip]     # Skip files that already exist"
      puts "  -q, [--quiet]    # Suppress status output"
      puts
      puts "Please choose a generator below."
      puts

      print_generators
    end

    def self.public_namespaces
      lookup!
      subclasses.map(&:namespace)
    end

    def self.print_generators
      sorted_groups.each { |b, n| print_list(b, n) }
    end

    def self.sorted_groups
      namespaces = public_namespaces
      namespaces.sort!

      groups = Hash.new { |h, k| h[k] = [] }
      namespaces.each do |namespace|
        base = namespace.split(":").first
        groups[base] << namespace
      end

      rails = groups.delete("rails")
      rails.map! { |n| n.sub(/^rails:/, "") }
      rails.delete("app")
      rails.delete("plugin")

      hidden_namespaces.each { |n| groups.delete(n.to_s) }

      [[ "rails", rails ]] + groups.sort.to_a
    end

    # Rails finds namespaces similar to thor, it only adds one rule:
    #
    # Generators names must end with "_generator.rb". This is required because Rails
    # looks in load paths and loads the generator just before it's going to be used.
    #
    #   find_by_namespace :webrat, :rails, :integration
    #
    # Will search for the following generators:
    #
    #   "rails:webrat", "webrat:integration", "webrat"
    #
    # Notice that "rails:generators:webrat" could be loaded as well, what
    # Rails looks for is the first and last parts of the namespace.
    def self.find_by_namespace(name, base = nil, context = nil) #:nodoc:
      lookups = []
      lookups << "#{base}:#{name}"    if base
      lookups << "#{name}:#{context}" if context

      unless base || context
        unless name.to_s.include?(?:)
          lookups << "#{name}:#{name}"
          lookups << "rails:#{name}"
        end
        lookups << "#{name}"
      end

      lookup(lookups)

      namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
      lookups.each do |namespace|

        klass = namespaces[namespace]
        return klass if klass
      end

      invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
    end

    # Receives a namespace, arguments and the behavior to invoke the generator.
    # It's used as the default entry point for generate, destroy and update
    # commands.
    def self.invoke(namespace, args = ARGV, config = {})
      names = namespace.to_s.split(":")
      if klass = find_by_namespace(names.pop, names.any? && names.join(":"))
        args << "--help" if args.empty? && klass.arguments.any?(&:required?)
        klass.start(args, config)
      else
        options     = sorted_groups.flat_map(&:last)
        suggestions = options.sort_by { |suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3)
        msg =  "Could not find generator '#{namespace}'. "
        msg << "Maybe you meant #{ suggestions.map { |s| "'#{s}'" }.to_sentence(last_word_connector: " or ", locale: :en) }\n"
        msg << "Run `rails generate --help` for more options."
        puts msg
      end
    end

    protected
      def self.print_list(base, namespaces)
        namespaces = namespaces.reject { |n| hidden_namespaces.include?(n) }
        super
      end

      # Try fallbacks for the given base.
      def self.invoke_fallbacks_for(name, base) #:nodoc:
        return nil unless base && fallbacks[base.to_sym]
        invoked_fallbacks = []

        Array(fallbacks[base.to_sym]).each do |fallback|
          next if invoked_fallbacks.include?(fallback)
          invoked_fallbacks << fallback

          klass = find_by_namespace(name, fallback)
          return klass if klass
        end

        nil
      end

      def self.command_type
        @command_type ||= "generator"
      end

      def self.lookup_paths
        @lookup_paths ||= %w( rails/generators generators )
      end

      def self.file_lookup_paths
        @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_generator.rb" ]
      end
  end
end