Class: Tap::Env::Constant

Inherits:
Object
  • Object
show all
Defined in:
lib/tap/env/constant.rb

Overview

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.

Constant Summary collapse

CONST_REGEXP =

Matches a valid constant

/\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(const_name, *require_paths) ⇒ Constant

Initializes a new Constant with the specified constant name, require_path, and comment. The const_name should be a valid constant name.



148
149
150
151
152
# File 'lib/tap/env/constant.rb', line 148

def initialize(const_name, *require_paths)
  @const_name = const_name
  @require_paths = require_paths
  @types = {}
end

Instance Attribute Details

#const_nameObject (readonly)

The full constant name



136
137
138
# File 'lib/tap/env/constant.rb', line 136

def const_name
  @const_name
end

#require_pathsObject (readonly)

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.



140
141
142
# File 'lib/tap/env/constant.rb', line 140

def require_paths
  @require_paths
end

#typesObject (readonly)

A hash of (type, summary) pairs used to classify self.



143
144
145
# File 'lib/tap/env/constant.rb', line 143

def types
  @types
end

Class Method Details

.constantize(const_name, const = Object) ⇒ Object

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.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/tap/env/constant.rb', line 68

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

.scan(dir, pattern = "**/*.rb", constants = {}) ⇒ Object

Scans the directory and pattern for constants and adds them to the constants hash by name.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/tap/env/constant.rb', line 89

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

Instance Method Details

#==(another) ⇒ Object

True if another is a Constant with the same const_name, require_path, and comment as self.



204
205
206
207
208
# File 'lib/tap/env/constant.rb', line 204

def ==(another)
  another.kind_of?(Constant) && 
  another.const_name == self.const_name &&
  another.require_paths == self.require_paths
end

#basenameObject

Returns the basename of path.

Constant.new("Const::Name").basename       # => 'name'


166
167
168
# File 'lib/tap/env/constant.rb', line 166

def basename
  @basename ||= File.basename(path)
end

#constantize(autorequire = true) ⇒ Object

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.



226
227
228
229
230
231
232
233
234
235
236
# File 'lib/tap/env/constant.rb', line 226

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

#dirnameObject

Returns the path, minus the basename of path.

Constant.new("Const::Name").dirname        # => 'const'


174
175
176
# File 'lib/tap/env/constant.rb', line 174

def dirname
  @dirname ||= (dirname = File.dirname(path)) == "." ? "" : dirname
end

#inspectObject

Returns a string like:

"#<Tap::Env::Constant:object_id Const::Name (require_path)>"


267
268
269
# File 'lib/tap/env/constant.rb', line 267

def inspect
  "#<#{self.class}:#{object_id} #{const_name} #{require_paths.inspect}>"
end

#nameObject

Returns the name of the constant, minus nesting.

Constant.new("Const::Name").name           # => 'Name'


182
183
184
# File 'lib/tap/env/constant.rb', line 182

def name
  @name ||= (const_name =~ /.*::(.*)\z/ ? $1 : const_name)
end

#nestingObject

Returns the nesting constant of const_name.

Constant.new("Const::Name").nesting        # => 'Const'


190
191
192
# File 'lib/tap/env/constant.rb', line 190

def nesting
  @nesting ||= (const_name =~ /(.*)::.*\z/ ? $1 : '')
end

#nesting_depthObject

Returns the number of constants in nesting.

Constant.new("Const::Name").nesting_depth  # => 1


198
199
200
# File 'lib/tap/env/constant.rb', line 198

def nesting_depth
  @nesting_depth ||= nesting.split(/::/).length
end

#pathObject

Returns the underscored const_name.

Constant.new("Const::Name").path           # => 'const/name'


158
159
160
# File 'lib/tap/env/constant.rb', line 158

def path
  @path ||= const_name.underscore
end

#register_as(type, summary = nil, override = false) ⇒ Object

Registers the type and summary with self. Raises an error if self is already registerd as the type and override is false.



212
213
214
215
216
217
218
219
# File 'lib/tap/env/constant.rb', line 212

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

#to_sObject

Returns const_name



272
273
274
# File 'lib/tap/env/constant.rb', line 272

def to_s
  const_name
end

#unload(unrequire = true) ⇒ Object

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.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/tap/env/constant.rb', line 248

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