Class: Tap::Env

Inherits:
Object
  • Object
show all
Includes:
Configurable, Enumerable, Minimap
Defined in:
lib/tap/env.rb,
lib/tap/env/gems.rb,
lib/tap/env/context.rb,
lib/tap/env/minimap.rb,
lib/tap/env/constant.rb,
lib/tap/env/manifest.rb,
lib/tap/env/string_ext.rb

Overview

Description

Env provides access to an execution environment spanning many directories, such as the working directory plus a series of gem directories. Envs merge the files from each directory into an abstract directory that may be globbed and accessed as a single unit. For example:

# /one
# |-- a.rb
# `-- b.rb
#
# /two
# |-- b.rb
# `-- c.rb
env =  Env.new('/one')
env << Env.new('/two')

env.collect {|e| e.root.root}
# => ["/one", "/two"]

env.glob(:root, "*.rb")
# => [
# "/one/a.rb",
# "/one/b.rb",
# "/two/c.rb"
# ]

As illustrated, files in the nested environment are accessible within the nesting environment. Envs provide methods for finding files associated with a specific class, and allow the generation of manifests that provide succinct access to various environment resources.

Usage of Envs is fairly straightforward, but the internals and default setup require some study as they have to span numerous functional domains. The most common features are detailed below.

Class Paths

Class paths are a kind of inheritance for files associated with a class. Say we had the following classes:

class A; end
class B < A; end

The naturally associated directories are ‘a’ and ‘b’. To look these up:

env.class_path(:root, A)    # => "/one/a"
env.class_path(:root, B)    # => "/one/b"

And to look up an associated file:

env.class_path(:root, A, "index.html") # => "/one/a/index.html"
env.class_path(:root, B, "index.html") # => "/one/b/index.html"

A block may be given to filter paths, for instance to test if a given file exists. The class_path method will check each env then roll up the inheritance hierarchy until the block returns true.

FileUtils.touch("/two/a/index.html")

visited_paths = []
env.class_path(:root, B, "index.html) do |path|
  visited_paths << path
  File.exists?(path)
end                         # => "/two/a/index.html"

visited_paths
# => [
# "/one/b/index.html",
# "/two/b/index.html",
# "/one/a/index.html",
# "/two/a/index.html"
# ]

This behavior is very useful for associating views with a class.

Manifest

Envs can generate manifests of various resources so they may be identified using minipaths (see Minimap for details regarding minipaths). Command files used by the tap executable are one example of a resource, and the constants used in building a workflow are another.

Manifest are generated by defining a builder, typically a block, that receives an env and returns an array of the associated resources. Using the same env as above:

manifest = env.manifest {|e| e.root.glob(:root, "*.rb") }

manifest.seek("a")          # => "/one/a.rb"
manifest.seek("b")          # => "/one/b.rb"
manifest.seek("c")          # => "/two/c.rb"

As illustrated, seek finds the first entry across all envs that matches the input minipath. A minipath for the env may be prepended to only search within a specific env.

manifest.seek("one:b")      # => "/one/b.rb"
manifest.seek("two:b")      # => "/two/b.rb"

Env caches a manifest of constants identified by constant attributes in files specified by under the Env.const_paths. These constants are used when interpreting signals from the command line. Constants may be manually registered to the constants manifest and classified by type like this:

class CustomTask
  def call; end
end
env.register(CustomTask).register_as(:task, "this is a custom task")

const = env.constants.seek('custom_task')
const.const_name           # => "CustomTask"
const.types                # => {:task => "this is a custom task"}
const.constantize          # => CustomTask

Setup

Envs may be manually setup in code by individually generating instances and nesting them. More commonly envs are defined in configuration files and instantiated by specifying where the files are located. The default config basename is ‘tap.yml’; any env_paths specified in the config file will be added.

This type of instantiation is recursive:

# [/one/tap.yml]
# env_paths: [/two]
#
# [/two/tap.yml]
# env_paths: [/three]
#

env = Env.new("/one", :basename => 'tap.yml')
env.collect {|e| e.root.root}
# => ["/one", "/two", "/three"]

Gem directories are fair game. Env allows specific gems to be specified by name (via the ‘gems’ config), and if a gem has a tap.yml file then it will be used to configure the gem env. Alternatively, an env may be set to automatically discover and nest gem environments. In this case gems are discovered when they have a tap.yml file.

ENV configs

Configurations may be also specified as an ENV variables. This type of configuration is very useful on the command line. Config variables should be prefixed by TAP_ and named like the capitalized config key (ex: TAP_GEMS or TAP_ENV_PATHS). See the Command Line Examples to see ENV configs in action.

These configurations may be accessed from Env#config, and are automatically incorporated by Env#setup.

Defined Under Namespace

Modules: Gems, Minimap, StringExt Classes: ConfigError, Constant, Context, Manifest

Constant Summary collapse

CONFIG_FILE =

The config file path

"tap.yml"
HOME =

The home directory for Tap

File.expand_path("#{File.dirname(__FILE__)}/../..")

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Minimap

#minihash, #minimap, #minimatch

Constructor Details

#initialize(config_or_dir = Dir.pwd, context = {}) ⇒ Env

Initializes a new Env linked to the specified directory. Configurations for the env will be loaded from the config file (as determined by the context) if it exists.

A configuration hash may be manually provided in the place of dir. In that case, no configurations will be loaded, even if the config file exists.

Context can be specified as a Context, or a Hash used to initialize a Context.



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/tap/env.rb', line 383

def initialize(config_or_dir=Dir.pwd, context={})
  
  # setup root
  config = nil
  @root = case config_or_dir
  when Root
    config_or_dir
  when String
    Root.new(config_or_dir)
  else
    config = config_or_dir
    
    if config.has_key?(:root) && config.has_key?('root')
      raise "multiple values mapped to :root"
    end
    
    root = config.delete(:root) || config.delete('root') || Dir.pwd
    root.kind_of?(Root) ? root : Root.new(root)
  end
  
  # note registration requires root.root, and so the
  # setup of context must follow the setup of root.
  @context = case context
  when Context
    context
  when Hash
    Context.new(context)
  else raise "cannot convert #{context.inspect} to Tap::Env::Context"
  end
  @context.register(self)
  
  # these need to be set for reset_env
  @active = false
  @gems = nil
  @env_paths = nil
  
  # only load configurations if configs were not provided
  config ||= Env.load_config(@context.config_file(@root.root))
  initialize_config(config)
  
  # set the invert flag
  @invert = false
end

Instance Attribute Details

#contextObject (readonly)

A Context tracking information shared among a set of envs.



308
309
310
# File 'lib/tap/env.rb', line 308

def context
  @context
end

#envsObject

An array of nested Envs, by default comprised of the env_path + gem environments (in that order). Envs can be manually set to override these defaults.



305
306
307
# File 'lib/tap/env.rb', line 305

def envs
  @envs
end

Class Method Details

.config(env_vars = ENV) ⇒ Object

Returns the Env configs specified in ENV. Config variables are prefixed by TAP_ and named like the capitalized config key (ex: TAP_GEMS or TAP_ENV_PATHS).



172
173
174
175
176
177
178
179
180
# File 'lib/tap/env.rb', line 172

def config(env_vars=ENV)
  config = {}
  env_vars.each_pair do |key, value|
    if key =~ /\ATAP_(.*)\z/
      config[$1.downcase] = value
    end
  end
  config
end

.load_config(path) ⇒ Object

Loads configurations from path as YAML. Returns an empty hash if the path loads to nil or false (as happens for empty files), or doesn’t exist.

Raises a ConfigError if the configurations do not load properly.



281
282
283
284
285
286
287
288
289
# File 'lib/tap/env.rb', line 281

def load_config(path)
  return {} unless path
  
  begin
    Root::Utils.trivial?(path) ? {} : (YAML.load_file(path) || {})
  rescue(Exception)
    raise ConfigError.new($!, path)
  end
end

.setup(dir = Dir.pwd, config_file = CONFIG_FILE) ⇒ Object

Initializes and activates an env as described in the config file under dir. The config file should be a relative path and will be used for determining configuration files under each env_path.

The env configuration is determined by merging the following in order:

The HOME directory for Tap will be added as an additonal environment if not already added somewhere in the env hierarchy. By default all gems will be included in the Env.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/tap/env.rb', line 194

def setup(dir=Dir.pwd, config_file=CONFIG_FILE)
  # setup configurations
  config = {'root' => dir, 'gems' => :all}
  
  user_config_file = config_file ? File.join(dir, config_file) : nil
  user = load_config(user_config_file)
  
  config.merge!(self.config)
  config.merge!(user)
  
  # keys must be symbolized as they are immediately 
  # used to initialize the Env configs
  config = config.inject({}) do |options, (key, value)|
    options[key.to_sym || key] = value
    options
  end
  
  # instantiate
  context = Context.new(:basename => config_file)
  env = new(config, context)
  
  # add the tap env if necessary
  unless env.any? {|e| e.root.root == HOME }
    env.push new(HOME, context) 
  end
  
  env.activate
  env
end

.setup_gem(gem_name, context = Context.new) ⇒ Object

Generates an Env for the specified gem or Gem::Specification. The gemspec for the gem is used to determine the env configuration in the following way:

root: the gem path
gems: all gem dependencies with a config_file
const_paths: the gem require paths
set_const_paths: false (because RubyGems sets them for you)

Configurations specified in the gem config_file override these defaults.



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/tap/env.rb', line 235

def setup_gem(gem_name, context=Context.new)
  spec = Gems.gemspec(gem_name)
  path = spec.full_gem_path
  
  # determine gem dependencies that have a config_file;
  # these will be set as the gems for the new Env
  dependencies = []
  spec.dependencies.each do |dependency|
    unless dependency.type == :runtime
      next
    end
    
    unless gemspec = Gems.gemspec(dependency)
      # this error may result when a dependency has
      # been uninstalled for a particular gem
      warn "missing gem dependency: #{dependency.to_s} (#{spec.full_name})"
      next
    end
    
    if config_file = context.config_file(gemspec.full_gem_path)
      next unless File.exists?(config_file)
    end
    
    dependencies << gemspec
  end
  
  config = {
    'root' => path,
    'gems' => dependencies,
    'const_paths' => spec.require_paths,
    'set_const_paths' => false
  }
  
  # override the default configs with whatever configs
  # are specified in the gem config file
  if config_file = context.config_file(path)
    config.merge!(load_config(config_file))
  end
  
  new(config, context)
end

Instance Method Details

#[](key, invert = invert?) ) ⇒ Object

Seeks a constant for the key, constantizing if necessary. If invert is true, this method seeks and returns a key for the input constant. In both cases AGET returns nil if no key-constant pair can be found.



674
675
676
677
678
679
680
681
682
683
684
685
686
687
# File 'lib/tap/env.rb', line 674

def [](key, invert=invert?)
  if invert
    const_name = key.to_s
    constants.unseek(true) do |const|
      const_name == const.const_name
    end
  else
    if constant = constants.seek(key)
      constant.constantize
    else
      nil
    end
  end
end

#activateObject

Activates self by doing the following, in order:

  • activate nested environments

  • unshift const_paths to $LOAD_PATH (if set_const_paths is true)

Once active, the current envs and const_paths are frozen and cannot be modified until deactivated. Returns true if activate succeeded, or false if self is already active.



499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# File 'lib/tap/env.rb', line 499

def activate
  return false if active?
  
  @active = true
  
  # freeze envs and const paths
  @envs.freeze
  @const_paths.freeze
  
  # activate nested envs
  envs.reverse_each do |env|
    env.activate
  end
  
  # add const paths
  if set_const_paths
    const_paths.reverse_each do |path|
      $LOAD_PATH.unshift(path)
    end
  
    $LOAD_PATH.uniq!
  end
  
  true
end

#active?Boolean

Return true if self has been activated.

Returns:

  • (Boolean)


563
564
565
# File 'lib/tap/env.rb', line 563

def active?
  @active
end

#class_path(dir, obj, *paths, &block) ⇒ Object

Returns the module_path traversing the inheritance hierarchy for the class of obj (or obj if obj is a Class). Included modules are not visited, only the superclasses.



633
634
635
636
637
# File 'lib/tap/env.rb', line 633

def class_path(dir, obj, *paths, &block)
  klass = obj.kind_of?(Class) ? obj : obj.class
  superclasses = klass.ancestors - klass.included_modules
  module_path(dir, superclasses, *paths, &block)
end

#constantsObject

Returns a manifest of Constants located in .rb files under each of the const_paths. Constants are identified using Lazydoc constant attributes; all attributes are registered to the constants for classification (for example as a task, join, etc).



656
657
658
659
660
661
662
663
664
665
666
667
668
669
# File 'lib/tap/env.rb', line 656

def constants
  @constants ||= manifest(:constants) do |env|
    constants = {}

    env.const_paths.each do |load_path|
      next unless File.directory?(load_path)
      Constant.scan(load_path, "**/*.rb", constants)
    end

    constants.keys.sort!.collect! do |key| 
      constants[key]
    end
  end
end

#deactivateObject

Deactivates self by doing the following in order:

  • deactivates nested environments

  • removes const_paths from $LOAD_PATH (if set_const_paths is true)

Once deactivated, envs and const_paths are unfrozen and may be modified. Returns true if deactivate succeeded, or false if self is not active.

Note

Deactivation does not necessarily leave $LOAD_PATH in the same condition as before activation. A pre-existing $LOAD_PATH entry can go missing if it is also registered as an env load_path (deactivation doesn’t know to leave such paths alone).

Deactivation, like constant unloading should be done with caution.



541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# File 'lib/tap/env.rb', line 541

def deactivate
  return false unless active?
  @active = false
  
  # dectivate nested envs
  envs.reverse_each do |env|
    env.deactivate
  end
  
  # remove const paths
  const_paths.each do |path|
    $LOAD_PATH.delete(path)
  end if set_const_paths
  
  # unfreeze envs and const paths
  @envs = @envs.dup
  @const_paths = @const_paths.dup
  
  true
end

#eachObject

Passes each nested env to the block in order, starting with self.



454
455
456
# File 'lib/tap/env.rb', line 454

def each
  visit_envs.each {|e| yield(e) }
end

#glob(dir, pattern = "**/*") ⇒ Object

Globs the abstract directory for files in the specified directory alias, matching the pattern. The expanded path of each matching file is returned.



570
571
572
# File 'lib/tap/env.rb', line 570

def glob(dir, pattern="**/*")
  hlob(dir, pattern).values.sort!
end

#hlob(dir, pattern = "**/*") ⇒ Object

Same as glob but returns results as a hash of (relative_path, path) pairs. In short the hash defines matching files in the abstract directory, linked to the actual path for these files.



577
578
579
580
581
582
583
584
585
586
587
# File 'lib/tap/env.rb', line 577

def hlob(dir, pattern="**/*")
  results = {}
  each do |env|
    root = env.root
    root.glob(dir, pattern).each do |path|
      relative_path = root.relative_path(dir, path)
      results[relative_path] ||= path
    end
  end
  results
end

#inspect(template = nil, globals = {}, filename = nil) ⇒ Object

When no template is specified, inspect generates a fairly standard inspection string. When a template is provided, inspect builds a Templater for each env with the following local variables:

variable    value
env         the current env
env_keys    a minihash for all envs

If a block is given, the globals and templater are yielded before any templater is built; this allows each env to add env-specific variables. After this preparation, each templater is built with the globals and the results concatenated.

The template is built with filename, if specified (for debugging).



752
753
754
755
756
757
758
759
760
761
762
763
764
765
# File 'lib/tap/env.rb', line 752

def inspect(template=nil, globals={}, filename=nil) # :yields: templater, globals
  if template == nil
    return "#<#{self.class}:#{object_id} root='#{root.root}'>" 
  end
  
  env_keys = minihash(true)
  collect do |env|
    templater = Templater.new(template, :env => env, :env_key => env_keys[env])
    yield(templater, globals) if block_given? 
    templater
  end.collect! do |templater|
    templater.build(globals, filename)
  end.join
end

#invertObject

Returns a duplicate of self with inverted AGET lookup.



702
703
704
# File 'lib/tap/env.rb', line 702

def invert
  dup.invert!
end

#invert!Object

Inverts the AGET lookup for self.



696
697
698
699
# File 'lib/tap/env.rb', line 696

def invert!
  @invert = !@invert
  self
end

#invert?Boolean

Indicates AGET looks up constants from keys (false), or keys from constants (true).

Returns:

  • (Boolean)


691
692
693
# File 'lib/tap/env.rb', line 691

def invert?
  @invert
end

#manifest(type = nil, &block) ⇒ Object

Generates a Manifest for self using the block as a builder. The builder receives an env and should return an array of resources, each of which can be minimappped. Minimapping requires that the resource is either a path string, or provides a ‘path’ method that returns a path string. Alternatively, a Minimap may be returned.

If a type is specified, then the manifest cache will be linked to the context cache.



647
648
649
650
# File 'lib/tap/env.rb', line 647

def manifest(type=nil, &block) # :yields: env
  cache = type ? (context.cache[type] ||= {}) : {}
  Manifest.new(self, block, cache)
end

#module_path(dir, modules, *paths, &block) ⇒ Object

Retrieves a path associated with the inheritance hierarchy of an object. An array of modules (which naturally can include classes) are provided and module_path traverses each, forming paths like:

path(dir, module_path, *paths)

By default ‘module_path’ is ‘module.to_s.underscore’ but modules can specify an alternative by providing a module_path method.

Paths are yielded to the block until the block returns true, at which point the current the path is returned. If no block is given, the first path is returned. Returns nil if the block never returns true.



613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
# File 'lib/tap/env.rb', line 613

def module_path(dir, modules, *paths, &block)
  paths.compact!
  while current = modules.shift
    module_path = if current.respond_to?(:module_path)
      current.module_path
    else
      current.to_s.underscore
    end
    
    if path = self.path(dir, module_path, *paths, &block)
      return path
    end
  end

  nil
end

#path(dir = :root, *paths) ⇒ Object

Returns the path to the specified file, as determined by root.

If a block is given, a path for each env will be yielded until the block returns a true value. Returns nil if the block never returns true.



593
594
595
596
597
598
599
# File 'lib/tap/env.rb', line 593

def path(dir = :root, *paths)
  each do |env|
    path = env.root.path(dir, *paths)
    return path if !block_given? || yield(path)
  end
  nil
end

#push(env) ⇒ Object Also known as: <<

Pushes env onto envs, removing duplicates.

Self cannot be pushed onto self.



444
445
446
447
448
449
450
# File 'lib/tap/env.rb', line 444

def push(env)
  unless env == self || envs[-1] == env
    envs = self.envs.reject {|e| e == env }
    self.envs = envs.push(env)
  end
  self
end

#recursive_inject(memo, &block) ⇒ Object

Recursively injects the memo to each env of self. Each env in envs receives the same memo from the parent. This is different from the inject provided via Enumerable, where each subsequent env receives the memo from the previous, not the parent, env.

a,b,c,d,e = ('a'..'e').collect {|name| Env.new(:name => name) }

a.push(b).push(c)
b.push(d).push(e)

lines = []
a.recursive_inject(0) do |nesting_depth, env|
  lines << "\n#{'..' * nesting_depth}#{env.config[:name]} (#{nesting_depth})"
  nesting_depth + 1
end

lines.join
# => %Q{
# a (0)
# ..b (1)
# ....d (2)
# ....e (2)
# ..c (1)}


487
488
489
# File 'lib/tap/env.rb', line 487

def recursive_inject(memo, &block) # :yields: memo, env
  inject_envs(memo, &block)
end

#register(constant) ⇒ Object

Registers a constant with self. The constant is stored as a new Constant in the constants manifest. Returns the new Constant. If the constant is already registered, the existing Constant is returned.



724
725
726
727
728
729
730
731
732
733
734
735
736
# File 'lib/tap/env.rb', line 724

def register(constant)
  const_name = constant.to_s
  entries = constants.entries(self)
  
  # try to find the existing Constant before making a new constant
  unless constant = entries.find {|const| const.const_name == const_name}
    constant = Constant.new(const_name)
    entries << constant
    entries.replace entries.sort_by {|const| const.const_name }
  end
  
  constant
end

#reverse_eachObject

Passes each nested env to the block in reverse order, ending with self.



459
460
461
# File 'lib/tap/env.rb', line 459

def reverse_each
  visit_envs.reverse_each {|e| yield(e) }
end

#scan(dir, pattern = "**/*.rb") ⇒ Object

Scans the files matched under the directory and pattern for constants and adds them to the existing constants for self.



708
709
710
711
712
713
714
715
716
717
718
# File 'lib/tap/env.rb', line 708

def scan(dir, pattern="**/*.rb")
  new_entries = {}
  entries = constants.entries(self)
  entries.each {|const| new_entries[const.const_name] = const }
  
  Constant.scan(root[dir], pattern, new_entries)
  
  entries.replace(new_entries.keys)
  entries.sort!.collect! {|key| new_entries[key] }
  entries
end

#unshift(env) ⇒ Object

Unshifts env onto envs. Self cannot be unshifted onto self.



435
436
437
438
439
440
# File 'lib/tap/env.rb', line 435

def unshift(env)
  unless env == self || envs[0] == env
    self.envs = envs.dup.unshift(env)
  end
  self
end