From daee6fd92ac16878f6806c3382a9e74592aa9656 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 7 Feb 2005 13:14:05 +0000 Subject: Added new generator framework that informs about its doings on generation and enables updating and destruction of generated artifacts. See the new script/destroy and script/update for more details #487 [bitsweat] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@518 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- railties/lib/rails_generator/lookup.rb | 204 +++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 railties/lib/rails_generator/lookup.rb (limited to 'railties/lib/rails_generator/lookup.rb') diff --git a/railties/lib/rails_generator/lookup.rb b/railties/lib/rails_generator/lookup.rb new file mode 100644 index 0000000000..ba47fd79be --- /dev/null +++ b/railties/lib/rails_generator/lookup.rb @@ -0,0 +1,204 @@ +require File.dirname(__FILE__) + '/spec' + +class Object + class << self + # Lookup missing generators using const_missing. This allows any + # generator to reference another without having to know its location: + # RubyGems, ~/.rails/generators, and RAILS_ROOT/script/generators all + # cooperate to get the job done. The greatest use of const_missing + # autoloading is to easily subclass existing generators. Example: + # class HorsebackGenerator < PostbackGenerator + # We don't know whether the postback generator is built in, installed + # as a gem, or in the user's home directory, and we shouldn't have to. + unless respond_to?(:pre_generator_const_missing) + alias_method :pre_generator_const_missing, :const_missing + + def const_missing(class_id) + if md = /(.+)Generator$/.match(class_id.to_s) + name = md.captures.first.demodulize.underscore + Rails::Generator::Base.lookup(name).klass + else + pre_generator_const_missing(class_id) + end + end + end + end +end + +# User home directory lookup adapted from RubyGems. +def Dir.user_home + if ENV['HOME'] + ENV['HOME'] + elsif ENV['USERPROFILE'] + ENV['USERPROFILE'] + elsif ENV['HOMEDRIVE'] and ENV['HOMEPATH'] + "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}" + else + File.expand_path '~' + end +end + + +module Rails + module Generator + + # Generator lookup is managed by a list of sources which return specs + # describing where to find and how to create generators. This module + # provides class methods for manipulating the source list and looking up + # generator specs, and an #instance wrapper for quickly instantiating + # generators by name. + # + # A spec is not a generator: it's a description of where to find + # the generator and how to create it. A source is anything that + # yields generators from #each. PathSource and GemSource are provided. + module Lookup + def self.append_features(base) + super + base.extend(ClassMethods) + base.use_component_sources! + end + + # Convenience method to instantiate another generator. + def instance(generator_name, args, runtime_options = {}) + self.class.instance(generator_name, args, runtime_options) + end + + module ClassMethods + # The list of sources where we look, in order, for generators. + def sources + read_inheritable_attribute(:sources) or use_component_sources! + end + + # Add a source to the end of the list. + def append_sources(*args) + sources.concat(args.flatten) + invalidate_cache! + end + + # Add a source to the beginning of the list. + def prepend_sources(*args) + write_inheritable_array(:sources, args.flatten + sources) + invalidate_cache! + end + + # Reset the source list. + def reset_sources + write_inheritable_attribute(:sources, []) + invalidate_cache! + end + + # Use application generators (app, ?). + def use_application_sources! + reset_sources + sources << PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/applications") + end + + # Use component generators (model, controller, etc). + # 1. Rails application. If RAILS_ROOT is defined we know we're + # generating in the context of a Rails application, so search + # RAILS_ROOT/script/generators. + # 2. User home directory. Search ~/.rails/generators. + # 3. RubyGems. Search for gems named *_generator. + # 4. Builtins. Model, controller, mailer, scaffold. + def use_component_sources! + reset_sources + sources << PathSource.new(:app, "#{Object.const_get(:RAILS_ROOT)}/script/generators") if Object.const_defined?(:RAILS_ROOT) + sources << PathSource.new(:user, "#{Dir.user_home}/.rails/generators") + sources << GemSource.new if Object.const_defined?(:Gem) + sources << PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/components") + end + + # Lookup knows how to find generators' Specs from a list of Sources. + # Searches the sources, in order, for the first matching name. + def lookup(generator_name) + @found ||= {} + generator_name = generator_name.to_s.downcase + @found[generator_name] ||= cache.find { |spec| + spec.name == generator_name + } or raise GeneratorError, "Couldn't find '#{generator_name}' generator" + end + + # Convenience method to lookup and instantiate a generator. + def instance(generator_name, args = [], runtime_options = {}) + lookup(generator_name).klass.new(args, full_options(runtime_options)) + end + + private + # Lookup and cache every generator from the source list. + def cache + @cache ||= sources.inject([]) { |cache, source| cache + source.map } + end + + # Clear the cache whenever the source list changes. + def invalidate_cache! + @cache = nil + end + end + end + + # Sources enumerate (yield from #each) generator specs which describe + # where to find and how to create generators. Enumerable is mixed in so, + # for example, source.collect will retrieve every generator. + # Sources may be assigned a label to distinguish them. + class Source + include Enumerable + + attr_reader :label + def initialize(label) + @label = label + end + + # The each method must be implemented in subclasses. + # The base implementation raises an error. + def each + raise NotImplementedError + end + + # Return a convenient sorted list of all generator names. + def names + map { |spec| spec.name }.sort + end + end + + + # PathSource looks for generators in a filesystem directory. + class PathSource < Source + attr_reader :path + + def initialize(label, path) + super label + @path = path + end + + # Yield each eligible subdirectory. + def each + Dir["#{path}/[a-z]*"].each do |dir| + if File.directory?(dir) + yield Spec.new(File.basename(dir), dir, label) + end + end + end + end + + + # GemSource hits the mines to quarry for generators. The latest versions + # of gems named *_generator are selected. + class GemSource < Source + def initialize + super :RubyGems + end + + # Yield latest versions of generator gems. + def each + Gem::cache.search(/_generator$/).inject({}) { |latest, gem| + hem = latest[gem.name] + latest[gem.name] = gem if hem.nil? or gem.version > hem.version + latest + }.values.each { |gem| + yield Spec.new(gem.name.sub(/_generator$/, ''), gem.full_gem_path, label) + } + end + end + + end +end -- cgit v1.2.3