Skip to content

Instantly share code, notes, and snippets.

@jnicklas
Created July 28, 2008 16:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnicklas/2916 to your computer and use it in GitHub Desktop.
Save jnicklas/2916 to your computer and use it in GitHub Desktop.
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_name>_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
# actual stuff that matters
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment