require 'tap/env/string_ext'
module Tap
class Env
# A Constant serves as a placeholder for an actual constant, sort of like
# autoload. Use the constantize method to retrieve the actual constant; if
# it doesn't exist, constantize requires require_path and tries again.
#
# Object.const_defined?(:Net) # => false
# $".include?('net/http') # => false
#
# http = Constant.new('Net::HTTP', 'net/http.rb')
# http.constantize # => Net::HTTP
# $".include?('net/http.rb') # => true
#
# === Unloading
#
# Constant also supports constant unloading. Unloading can be useful in
# various development modes, but may cause code to behave unpredictably.
# When a Constant unloads, the constant value is removed from the nesting
# constant and the require paths are removed from $". This allows a
# require statement to re-require, and in theory, reload the constant.
#
# # [simple.rb]
# # class Simple
# # end
#
# const = Constant.new('Simple', 'simple')
# const.constantize # => Simple
# Object.const_defined?(:Simple) # => true
#
# const.unload # => Simple
# Object.const_defined?(:Simple) # => false
#
# const.constantize # => Simple
# Object.const_defined?(:Simple) # => true
#
# Unloading and reloading works best for scripts that have no side effects;
# ie scripts that do not require other files and only define the specified
# class or module.
#
#--
# ==== Rationale for that last statement
#
# Scripts that require other files will not re-require the other files
# because unload doesn't remove the other files from $". Likewise scripts
# that define other constants effectively overwrite the existing constant;
# that may or may not be a big deal, but it can cause warnings. Moreover,
# if a script actually DOES something (like create a file), that something
# will be repeated when it gets re-required.
#
class Constant
class << self
# Constantize tries to look up the specified constant under const. A
# block may be given to manually look up missing constants; the last
# existing const and any non-existant constant names are yielded to the
# block, which is expected to return the desired constant. For instance
# in the example 'Non::Existant' is essentially mapping to ConstName.
#
# module ConstName; end
#
# Constant.constantize('ConstName') # => ConstName
# Constant.constantize('Non::Existant') { ConstName } # => ConstName
#
# Raises a NameError for invalid/missing constants.
def constantize(const_name, const=Object) # :yields: const, missing_const_names
unless CONST_REGEXP =~ const_name
raise NameError, "#{const_name.inspect} is not a valid constant name!"
end
constants = $1.split(/::/)
while !constants.empty?
unless const_is_defined?(const, constants[0])
if block_given?
return yield(const, constants)
else
raise NameError.new("uninitialized constant #{const_name}", constants[0])
end
end
const = const.const_get(constants.shift)
end
const
end
# Scans the directory and pattern for constants and adds them to the
# constants hash by name.
def scan(dir, pattern="**/*.rb", constants={})
if pattern.include?("..")
raise "patterns cannot include relative paths: #{pattern.inspect}"
end
# note changing dir here makes require paths relative to load_path,
# hence they can be directly converted into a default_const_name
# rather than first performing Root.relative_path
Dir.chdir(dir) do
Dir.glob(pattern).each do |path|
default_const_name = path.chomp(File.extname(path)).camelize
# scan for constants
Lazydoc::Document.scan(File.read(path)) do |const_name, type, summary|
const_name = default_const_name if const_name.empty?
constant = (constants[const_name] ||= Constant.new(const_name))
constant.register_as(type, summary)
constant.require_paths << path
end
end
end
constants
end
private
# helper method. Determines if the named constant is defined in const.
# The implementation has to be different for ruby 1.9 due to changes
# in the API.
case RUBY_VERSION
when /^1.9/
def const_is_defined?(const, const_name) # :nodoc:
const.const_defined?(const_name, false)
end
else
def const_is_defined?(const, const_name) # :nodoc:
const.const_defined?(const_name)
end
end
end
# Matches a valid constant
CONST_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/
# The full constant name
attr_reader :const_name
# An array of paths that will be required when the constantize is called
# and the constant does not exist. Require paths are required in order.
attr_reader :require_paths
# A hash of (type, summary) pairs used to classify self.
attr_reader :types
# Initializes a new Constant with the specified constant name,
# require_path, and comment. The const_name should be a valid
# constant name.
def initialize(const_name, *require_paths)
@const_name = const_name
@require_paths = require_paths
@types = {}
end
# Returns the underscored const_name.
#
# Constant.new("Const::Name").path # => 'const/name'
#
def path
@path ||= const_name.underscore
end
# Returns the basename of path.
#
# Constant.new("Const::Name").basename # => 'name'
#
def basename
@basename ||= File.basename(path)
end
# Returns the path, minus the basename of path.
#
# Constant.new("Const::Name").dirname # => 'const'
#
def dirname
@dirname ||= (dirname = File.dirname(path)) == "." ? "" : dirname
end
# Returns the name of the constant, minus nesting.
#
# Constant.new("Const::Name").name # => 'Name'
#
def name
@name ||= (const_name =~ /.*::(.*)\z/ ? $1 : const_name)
end
# Returns the nesting constant of const_name.
#
# Constant.new("Const::Name").nesting # => 'Const'
#
def nesting
@nesting ||= (const_name =~ /(.*)::.*\z/ ? $1 : '')
end
# Returns the number of constants in nesting.
#
# Constant.new("Const::Name").nesting_depth # => 1
#
def nesting_depth
@nesting_depth ||= nesting.split(/::/).length
end
# True if another is a Constant with the same const_name,
# require_path, and comment as self.
def ==(another)
another.kind_of?(Constant) &&
another.const_name == self.const_name &&
another.require_paths == self.require_paths
end
# Registers the type and summary with self. Raises an error if self is
# already registerd as the type and override is false.
def register_as(type, summary=nil, override=false)
if types.include?(type) && !override
raise "already registered as a #{type.inspect}"
end
types[type] = summary
self
end
# Looks up and returns the constant indicated by const_name. If the
# constant cannot be found, constantize requires the require_paths
# in order and tries again.
#
# Raises a NameError if the constant cannot be found.
def constantize(autorequire=true)
Constant.constantize(const_name) do
break unless autorequire
require_paths.each do |require_path|
require require_path
end
Constant.constantize(const_name)
end
end
# Undefines the constant indicated by const_name. The nesting constants
# are not removed. If specified, the require_paths will be removed from $".
#
# When removing require_path, unload will add '.rb' to the require_path if
# require_path has no extension (this echos the behavior of require).
# Other extension names like '.so', '.dll', etc. are not tried and will
# not be removed.
#
# Does nothing if const_name doesn't exist. Returns the unloaded constant.
# Obviously, this method should be used with caution.
def unload(unrequire=true)
const = nesting.empty? ? Object : Constant.constantize(nesting) { Object }
if const.const_defined?(name)
require_paths.each do |require_path|
path = File.extname(require_path).empty? ? "#{require_path}.rb" : require_path
$".delete(path)
end if unrequire
return const.send(:remove_const, name)
end
nil
end
# Returns a string like:
#
# "#"
#
def inspect
"#<#{self.class}:#{object_id} #{const_name} #{require_paths.inspect}>"
end
# Returns const_name
def to_s
const_name
end
end
end
end