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, ~/.rubigen/generators, and APP_ROOT/generators. def lookup_missing_generator(class_id) if md = /(.+)Generator$/.match(class_id.to_s) name = md.captures.first.demodulize.underscore RubiGen::Base.lookup(name).klass else const_missing_before_generators(class_id) end end unless respond_to?(:const_missing_before_generators) alias_method :const_missing_before_generators, :const_missing alias_method :const_missing, :lookup_missing_generator 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 RubiGen # 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 GemGeneratorSource are provided. module Lookup def self.included(base) 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 if read_inheritable_attribute(:sources).blank? if superclass == RubiGen::Base superclass_sources = superclass.sources diff = superclass_sources.inject([]) do |mem, source| found = false application_sources.each { |app_source| found ||= true if app_source == source} mem << source unless found mem end write_inheritable_attribute(:sources, diff) end use_component_sources! if read_inheritable_attribute(:sources).blank? end read_inheritable_attribute(: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!(*filters) reset_sources write_inheritable_attribute(:sources, application_sources(filters)) end def application_sources(filters = []) filters.unshift 'app' app_sources = [] app_sources << PathSource.new(:builtin, File.join(File.dirname(__FILE__), %w[.. .. app_generators])) app_sources << filtered_sources(filters) app_sources.flatten end # Use component generators (test_unit, etc). # 1. Current application. If APP_ROOT is defined we know we're # generating in the context of this application, so search # APP_ROOT/generators. # 2. User home directory. Search ~/.rubigen/generators. # 3. RubyGems. Search for gems containing /{scope}_generators folder. # 4. Builtins. None currently. # # Search can be filtered by passing one or more prefixes. # e.g. use_component_sources!(:rubygems) means it will also search in the following # folders: # 5. User home directory. Search ~/.rubigen/rubygems_generators. # 6. RubyGems. Search for gems containing /rubygems_generators folder. def use_component_sources!(*filters) reset_sources new_sources = [] if defined? ::APP_ROOT new_sources << PathSource.new(:root, "#{::APP_ROOT}/generators") new_sources << PathSource.new(:vendor, "#{::APP_ROOT}/vendor/generators") new_sources << PathSource.new(:plugins, "#{::APP_ROOT}/vendor/plugins/*/**/generators") end new_sources << filtered_sources(filters) write_inheritable_attribute(:sources, new_sources.flatten) end def filtered_sources(filters) new_sources = [] new_sources << PathFilteredSource.new(:user, "#{Dir.user_home}/.rubigen/", *filters) if Object.const_defined?(:Gem) new_sources << GemPathSource.new(*filters) end new_sources 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 } unless @found[generator_name] chars = generator_name.scan(/./).map{|c|"#{c}.*?"} rx = /^#{chars}$/ gns = cache.select{|spec| spec.name =~ rx } @found[generator_name] ||= gns.first if gns.length == 1 raise GeneratorError, "Pattern '#{generator_name}' matches more than one generator: #{gns.map{|sp|sp.name}.join(', ')}" if gns.length > 1 end @found[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(filter = nil) inject([]) do |mem, spec| case filter when :visible mem << spec.name if spec.visible? else usage_message end mem end.sort end end # PathSource looks for generators in a filesystem directory. class PathSource < Source attr_reader :path def initialize(label, path) super label @path = File.expand_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 def ==(source) self.class == source.class && path == source.path end end class PathFilteredSource < PathSource attr_reader :filters def initialize(label, path, *filters) super label, File.join(path, "#{filter_str(filters)}generators") end def filter_str(filters) @filters = filters.first.is_a?(Array) ? filters.first : filters return "" if @filters.blank? filter_str = @filters.map {|filter| "#{filter}_"}.join(",") filter_str += "," "{#{filter_str}}" end def ==(source) self.class == source.class && path == source.path && filters == source.filters && label == source.label end end class AbstractGemSource < Source def initialize super :RubyGems end end # GemPathSource looks for generators within any RubyGem's /{filter_}generators/**/_generator.rb file. class GemPathSource < AbstractGemSource attr_accessor :filters def initialize(*filters) super() @filters = filters end # Yield each generator within rails_generator subdirectories. def each generator_full_paths.each do |generator| yield Spec.new(File.basename(generator).sub(/_generator.rb$/, ''), File.dirname(generator), label) end end def ==(source) self.class == source.class && filters == source.filters end private def generator_full_paths @generator_full_paths ||= Gem::cache.inject({}) do |latest, name_gem| name, gem = name_gem hem = latest[gem.name] latest[gem.name] = gem if hem.nil? or gem.version > hem.version latest end.values.inject([]) do |mem, gem| Dir[gem.full_gem_path + "/#{filter_str}generators/**/*_generator.rb"].each do |generator| mem << generator end mem end.reverse end def filter_str @filters = filters.first.is_a?(Array) ? filters.first : filters return "" if filters.blank? filter_str = filters.map {|filter| "#{filter}_"}.join(",") filter_str += "," "{#{filter_str}}" end end end