aboutsummaryrefslogtreecommitdiffstats
path: root/railties/lib/rails/generators.rb
diff options
context:
space:
mode:
Diffstat (limited to 'railties/lib/rails/generators.rb')
-rw-r--r--railties/lib/rails/generators.rb373
1 files changed, 373 insertions, 0 deletions
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
new file mode 100644
index 0000000000..bf2390cb7e
--- /dev/null
+++ b/railties/lib/rails/generators.rb
@@ -0,0 +1,373 @@
+activesupport_path = File.expand_path('../../../../activesupport/lib', __FILE__)
+$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
+
+require 'thor/group'
+
+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
+ 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',
+ template_engine: '-e',
+ test_framework: '-t'
+ },
+
+ test_unit: {
+ fixture_replacement: '-r',
+ }
+ }
+
+ DEFAULT_OPTIONS = {
+ rails: {
+ 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,
+ test_framework: false,
+ template_engine: :erb
+ }
+ }
+
+ def self.configure!(config) #:nodoc:
+ 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
+
+ # Remove the color from output.
+ def self.no_color!
+ Thor::Base.shell = Thor::Shell::Basic
+ end
+
+ # Track all generators subclasses.
+ def self.subclasses
+ @subclasses ||= []
+ 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? { |a| a.required? }
+ klass.start(args, config)
+ else
+ options = sorted_groups.map(&:last).flatten
+ 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}'"}.join(" or ") }\n"
+ msg << "Run `rails generate --help` for more options."
+ puts msg
+ end
+ 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",
+ "#{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 { |k| k.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
+
+ protected
+
+ # This code is based directly on the Text gem implementation
+ # Returns a value representing the "cost" of transforming str1 into str2
+ def self.levenshtein_distance str1, str2
+ s = str1
+ t = str2
+ n = s.length
+ m = t.length
+ max = n/2
+
+ return m if (0 == n)
+ return n if (0 == m)
+ return n if (n - m).abs > max
+
+ d = (0..m).to_a
+ x = nil
+
+ str1.each_char.each_with_index do |char1,i|
+ e = i+1
+
+ str2.each_char.each_with_index do |char2,j|
+ cost = (char1 == char2) ? 0 : 1
+ x = [
+ d[j+1] + 1, # insertion
+ e + 1, # deletion
+ d[j] + cost # substitution
+ ].min
+ d[j] = e
+ e = x
+ end
+
+ d[m] = x
+ end
+
+ return x
+ end
+
+ # Prints a list of generators.
+ def self.print_list(base, namespaces) #:nodoc:
+ namespaces = namespaces.reject do |n|
+ hidden_namespaces.include?(n)
+ end
+
+ return if namespaces.empty?
+ puts "#{base.camelize}:"
+
+ namespaces.each do |namespace|
+ puts(" #{namespace}")
+ end
+
+ puts
+ 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
+
+ # Receives namespaces in an array and tries to find matching generators
+ # in the load path.
+ def self.lookup(namespaces) #:nodoc:
+ paths = namespaces_to_paths(namespaces)
+
+ paths.each do |raw_path|
+ ["rails/generators", "generators"].each do |base|
+ path = "#{base}/#{raw_path}_generator"
+
+ begin
+ require path
+ return
+ rescue LoadError => e
+ raise unless e.message =~ /#{Regexp.escape(path)}$/
+ rescue Exception => e
+ warn "[WARNING] Could not load generator #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
+ end
+ end
+ end
+ end
+
+ # This will try to load any generator in the load path to show in help.
+ def self.lookup! #:nodoc:
+ $LOAD_PATH.each do |base|
+ Dir[File.join(base, "{rails/generators,generators}", "**", "*_generator.rb")].each do |path|
+ begin
+ path = path.sub("#{base}/", "")
+ require path
+ rescue Exception
+ # No problem
+ end
+ end
+ end
+ end
+
+ # Convert namespaces to paths by replacing ":" for "/" and adding
+ # an extra lookup. For example, "rails:model" should be searched
+ # in both: "rails/model/model_generator" and "rails/model_generator".
+ def self.namespaces_to_paths(namespaces) #:nodoc:
+ paths = []
+ namespaces.each do |namespace|
+ pieces = namespace.split(":")
+ paths << pieces.dup.push(pieces.last).join("/")
+ paths << pieces.join("/")
+ end
+ paths.uniq!
+ paths
+ end
+ end
+end