tap 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,115 @@
1
+ require 'configurable'
2
+ require 'tap/signals/help'
3
+
4
+ module Tap
5
+ class App
6
+
7
+ # Api implements the application interface described in the
8
+ # API[link:files/doc/API.html] document, and provides additional
9
+ # functionality shared by the Tap base classes.
10
+ class Api
11
+ class << self
12
+
13
+ # The type of the class.
14
+ attr_reader :type
15
+
16
+ def inherited(child) # :nodoc:
17
+ super
18
+
19
+ type = self.type || child.to_s.split('::').last.downcase
20
+ child.instance_variable_set(:@type, type)
21
+
22
+ unless child.respond_to?(:desc)
23
+ child.lazy_attr(:desc, type)
24
+ end
25
+ end
26
+
27
+ # Returns a ConfigParser setup to parse the configurations for the
28
+ # subclass. The parser is also setup to print usage (using the desc
29
+ # for the subclass) and exit for the '-h' and '--help' options.
30
+ #
31
+ # The parse method uses parser by default, so subclasses can simply
32
+ # modify parser and ensure parse still works correctly.
33
+ def parser
34
+ opts = ConfigParser.new
35
+
36
+ unless configurations.empty?
37
+ opts.separator "configurations:"
38
+ opts.add(configurations)
39
+ opts.separator ""
40
+ end
41
+
42
+ opts.separator "options:"
43
+
44
+ # add option to print help
45
+ opts.on("-h", "--help", "Print this help") do
46
+ puts "#{self}#{desc.empty? ? '' : ' -- '}#{desc.to_s}"
47
+ puts help
48
+ puts opts
49
+ exit
50
+ end
51
+
52
+ opts
53
+ end
54
+
55
+ # Parses the argv into an instance of self. Internally parse parses
56
+ # an argh then calls build, but there is no requirement that this
57
+ # occurs in subclasses.
58
+ def parse(argv=ARGV, app=Tap::App.instance)
59
+ parse!(argv.dup, app)
60
+ end
61
+
62
+ # Same as parse, but removes arguments destructively.
63
+ def parse!(argv=ARGV, app=Tap::App.instance)
64
+ parser = self.parser
65
+ argv = parser.parse!(argv, :add_defaults => false)
66
+
67
+ [build({'config' => parser.nested_config}, app), argv]
68
+ end
69
+
70
+ # Returns an instance of self. By default build calls new with the
71
+ # configurations specified by spec['config'], and app.
72
+ def build(spec={}, app=Tap::App.instance)
73
+ new(spec['config'] || {}, app)
74
+ end
75
+
76
+ # Returns a help string that formats the desc documentation.
77
+ def help
78
+ lines = desc.kind_of?(Lazydoc::Comment) ? desc.wrap(77, 2, nil) : []
79
+ lines.collect! {|line| " #{line}"}
80
+ unless lines.empty?
81
+ line = '-' * 80
82
+ lines.unshift(line)
83
+ lines.push(line)
84
+ end
85
+
86
+ lines.join("\n")
87
+ end
88
+ end
89
+
90
+ include Configurable
91
+ include Signals
92
+
93
+ signal :help, :class => Help, :bind => nil # signals help
94
+
95
+ # The App receiving self during enq
96
+ attr_reader :app
97
+
98
+ def initialize(config={}, app=Tap::App.instance)
99
+ @app = app
100
+ initialize_config(config)
101
+ end
102
+
103
+ # By default associations returns nil.
104
+ def associations
105
+ end
106
+
107
+ # By default to_spec returns a hash like {'config' => config} where
108
+ # config is a stringified representation of the configurations for self.
109
+ def to_spec
110
+ config = self.config.to_hash {|hash, key, value| hash[key.to_s] = value }
111
+ {'config' => config}
112
+ end
113
+ end
114
+ end
115
+ end
@@ -5,71 +5,70 @@ module Tap
5
5
 
6
6
  # Queue allows thread-safe enqueing and dequeing of nodes and inputs for
7
7
  # execution.
8
+ #
9
+ # === API
10
+ #
11
+ # The following methods are required in alternative implementations of an
12
+ # applicaton queue, where a job is a [node, inputs] array:
13
+ #
14
+ # enq(node, inputs) # pushes the job onto the queue
15
+ # unshift(node, inputs) # unshifts the job onto the queue
16
+ # deq # shifts a job off the queue
17
+ # size # returns the number of jobs in the queue
18
+ # clear # clears the queue, returns current jobs
19
+ # synchronize # yields to the block
20
+ # to_a # returns the queue as an array
21
+ #
22
+ # Note that synchronize must be implemented even if it does nothing but
23
+ # yield to the block.
8
24
  class Queue < Monitor
9
25
 
10
- attr_reader :queue
11
-
12
26
  # Creates a new Queue
13
27
  def initialize
14
28
  super
15
29
  @queue = []
16
30
  end
17
31
 
18
- # Clears self and returns an array of the enqueued methods and inputs,
19
- # organized by round.
20
- def clear
32
+ # Enqueues the node and inputs as a job.
33
+ def enq(node, inputs)
21
34
  synchronize do
22
- current = queue
23
- @queue = []
24
- current
35
+ @queue.push [node, inputs]
25
36
  end
26
37
  end
27
38
 
28
- # Returns the number of enqueued methods
29
- def size
39
+ # Enqueues the node and inputs, but to the top of the queue.
40
+ def unshift(node, inputs)
30
41
  synchronize do
31
- queue.size
42
+ @queue.unshift [node, inputs]
32
43
  end
33
44
  end
34
45
 
35
- # True if no methods are enqueued
36
- def empty?
37
- synchronize { size == 0 }
38
- end
39
-
40
- # Enqueues the method and inputs.
41
- def enq(method, inputs)
42
- synchronize do
43
- queue.push [method, inputs]
44
- end
46
+ # Dequeues the next job as an array like [node, inputs]. Returns nil if
47
+ # the queue is empty.
48
+ def deq
49
+ synchronize { @queue.shift }
45
50
  end
46
51
 
47
- # Enqueues the method and inputs, but to the top of the queue.
48
- def unshift(method, inputs)
52
+ # Returns the number of enqueued jobs.
53
+ def size
49
54
  synchronize do
50
- queue.unshift [method, inputs]
55
+ @queue.size
51
56
  end
52
57
  end
53
58
 
54
- # Concats an array of [method, input] entries to self.
55
- def concat(entries)
59
+ # Clears self and returns an array of the currently enqueued jobs.
60
+ def clear
56
61
  synchronize do
57
- entries.each do |method, inputs|
58
- enq(method, inputs)
59
- end
62
+ current = @queue
63
+ @queue = []
64
+ current
60
65
  end
61
66
  end
62
67
 
63
- # Dequeues the next method and inputs as an array like
64
- # [method, inputs]. Returns nil if the queue is empty.
65
- def deq
66
- synchronize { queue.shift }
67
- end
68
-
69
- # Converts self to an array.
68
+ # Returns the enqueued jobs as an array.
70
69
  def to_a
71
70
  synchronize do
72
- queue.dup
71
+ @queue.dup
73
72
  end
74
73
  end
75
74
  end
@@ -15,7 +15,8 @@ module Tap
15
15
  # State.state_str(0) # => 'READY'
16
16
  # State.state_str(12) # => nil
17
17
  def state_str(state)
18
- constants.inject(nil) {|str, s| const_get(s) == state ? s.to_s : str}
18
+ const = constants.find {|const_name| const_get(const_name) == state }
19
+ const ? const.to_s : nil
19
20
  end
20
21
  end
21
22
  end
@@ -1,24 +1,243 @@
1
1
  require 'tap/root'
2
+ require 'tap/env/constant'
3
+ require 'tap/env/context'
2
4
  require 'tap/env/manifest'
3
5
  require 'tap/templater'
4
6
  autoload(:YAML, 'yaml')
5
7
 
6
8
  module Tap
7
- # Env abstracts an execution environment that spans many directories.
9
+
10
+ # == Description
11
+ #
12
+ # Env provides access to an execution environment spanning many directories,
13
+ # such as the working directory plus a series of gem directories. Envs merge
14
+ # the files from each directory into an abstract directory that may be
15
+ # globbed and accessed as a single unit. For example:
16
+ #
17
+ # # /one
18
+ # # |-- a.rb
19
+ # # `-- b.rb
20
+ # #
21
+ # # /two
22
+ # # |-- b.rb
23
+ # # `-- c.rb
24
+ # env = Env.new('/one')
25
+ # env << Env.new('/two')
26
+ #
27
+ # env.collect {|e| e.root.root}
28
+ # # => ["/one", "/two"]
29
+ #
30
+ # env.glob(:root, "*.rb")
31
+ # # => [
32
+ # # "/one/a.rb",
33
+ # # "/one/b.rb",
34
+ # # "/two/c.rb"
35
+ # # ]
36
+ #
37
+ # As illustrated, files in the nested environment are accessible within the
38
+ # nesting environment. Envs provide methods for finding files associated
39
+ # with a specific class, and allow the generation of manifests that provide
40
+ # succinct access to various environment resources.
41
+ #
42
+ # Usage of Envs is fairly straightforward, but the internals and default
43
+ # setup require some study as they have to span numerous functional domains.
44
+ # The most common features are detailed below.
45
+ #
46
+ # ==== Class Paths
47
+ #
48
+ # Class paths are a kind of inheritance for files associated with a class.
49
+ # Say we had the following classes:
50
+ #
51
+ # class A; end
52
+ # class B < A; end
53
+ #
54
+ # The naturally associated directories are 'a' and 'b'. To look these up:
55
+ #
56
+ # env.class_path(:root, A) # => "/one/a"
57
+ # env.class_path(:root, B) # => "/one/b"
58
+ #
59
+ # And to look up an associated file:
60
+ #
61
+ # env.class_path(:root, A, "index.html") # => "/one/a/index.html"
62
+ # env.class_path(:root, B, "index.html") # => "/one/b/index.html"
63
+ #
64
+ # A block may be given to filter paths, for instance to test if a given file
65
+ # exists. The class_path method will check each env then roll up the
66
+ # inheritance hierarchy until the block returns true.
67
+ #
68
+ # FileUtils.touch("/two/a/index.html")
69
+ #
70
+ # visited_paths = []
71
+ # env.class_path(:root, B, "index.html) do |path|
72
+ # visited_paths << path
73
+ # File.exists?(path)
74
+ # end # => "/two/a/index.html"
75
+ #
76
+ # visited_paths
77
+ # # => [
78
+ # # "/one/b/index.html",
79
+ # # "/two/b/index.html",
80
+ # # "/one/a/index.html",
81
+ # # "/two/a/index.html"
82
+ # # ]
83
+ #
84
+ # This behavior is very useful for associating views with a class.
85
+ #
86
+ # ==== Manifest
87
+ #
88
+ # Envs can generate manifests of various resources so they may be identified
89
+ # using minipaths (see Minimap for details regarding minipaths). Command
90
+ # files used by the tap executable are one example of a resource, and the
91
+ # constants used in building a workflow are another.
92
+ #
93
+ # Manifest are generated by defining a builder, typically a block, that
94
+ # receives an env and returns an array of the associated resources.
95
+ # Using the same env as above:
96
+ #
97
+ # manifest = env.manifest {|e| e.root.glob(:root, "*.rb") }
98
+ #
99
+ # manifest.seek("a") # => "/one/a.rb"
100
+ # manifest.seek("b") # => "/one/b.rb"
101
+ # manifest.seek("c") # => "/two/c.rb"
102
+ #
103
+ # As illustrated, seek finds the first entry across all envs that matches the
104
+ # input minipath. A minipath for the env may be prepended to only search
105
+ # within a specific env.
106
+ #
107
+ # manifest.seek("one:b") # => "/one/b.rb"
108
+ # manifest.seek("two:b") # => "/two/b.rb"
109
+ #
110
+ # Env caches a manifest of constants identified by {constant attributes}[http://tap.rubyforge.org/lazydoc]
111
+ # in files specified by under the Env.const_paths. These constants are used
112
+ # when interpreting signals from the command line. Constants may be manually
113
+ # registered to the constants manifest and classified by type like this:
114
+ #
115
+ # class CustomTask
116
+ # def call; end
117
+ # end
118
+ # env.register(CustomTask).register_as(:task, "this is a custom task")
119
+ #
120
+ # const = env.constants.seek('custom_task')
121
+ # const.const_name # => "CustomTask"
122
+ # const.types # => {:task => "this is a custom task"}
123
+ # const.constantize # => CustomTask
124
+ #
125
+ # == Setup
126
+ #
127
+ # Envs may be manually setup in code by individually generating instances
128
+ # and nesting them. More commonly envs are defined in configuration files
129
+ # and instantiated by specifying where the files are located. The default
130
+ # config basename is 'tap.yml'; any env_paths specified in the config file
131
+ # will be added.
132
+ #
133
+ # This type of instantiation is recursive:
134
+ #
135
+ # # [/one/tap.yml]
136
+ # # env_paths: [/two]
137
+ # #
138
+ # # [/two/tap.yml]
139
+ # # env_paths: [/three]
140
+ # #
141
+ #
142
+ # env = Env.new("/one", :basename => 'tap.yml')
143
+ # env.collect {|e| e.root.root}
144
+ # # => ["/one", "/two", "/three"]
145
+ #
146
+ # Gem directories are fair game. Env allows specific gems to be specified
147
+ # by name (via the 'gems' config), and if a gem has a tap.yml file then it
148
+ # will be used to configure the gem env. Alternatively, an env may be set
149
+ # to automatically discover and nest gem environments. In this case gems
150
+ # are discovered when they have a tap.yml file.
151
+ #
152
+ # ==== ENV configs
153
+ #
154
+ # Configurations may be also specified as an ENV variables. This type of
155
+ # configuration is very useful on the command line. Config variables
156
+ # should be prefixed by TAP_ and named like the capitalized config key
157
+ # (ex: TAP_GEMS or TAP_ENV_PATHS). See the
158
+ # {Command Line Examples}[link:files/doc/Examples/Command%20Line.html]
159
+ # to see ENV configs in action.
160
+ #
161
+ # These configurations may be accessed from Env#config, and are
162
+ # automatically incorporated by Env#setup.
163
+ #
8
164
  class Env
9
165
  autoload(:Gems, 'tap/env/gems')
10
166
 
11
167
  class << self
12
- attr_writer :instance
13
168
 
14
- def instance(auto_initialize=true)
15
- @instance ||= (auto_initialize ? new : nil)
169
+ # Returns the Env configs specified in ENV. Config variables are
170
+ # prefixed by TAP_ and named like the capitalized config key
171
+ # (ex: TAP_GEMS or TAP_ENV_PATHS).
172
+ def config(env_vars=ENV)
173
+ config = {}
174
+ env_vars.each_pair do |key, value|
175
+ if key =~ /\ATAP_(.*)\z/
176
+ config[$1.downcase] = value
177
+ end
178
+ end
179
+ config
16
180
  end
17
181
 
18
- def from_gemspec(spec, context={})
182
+ # Initializes and activates an env as described in the config file under
183
+ # dir. The config file should be a relative path and will be used for
184
+ # determining configuration files under each env_path.
185
+ #
186
+ # The env configuration is determined by merging the following in order:
187
+ # * defaults {root => dir, gems => all}
188
+ # * ENV configs
189
+ # * config_file configs
190
+ #
191
+ # The HOME directory for Tap will be added as an additonal environment
192
+ # if not already added somewhere in the env hierarchy. By default all
193
+ # gems will be included in the Env.
194
+ def setup(dir=Dir.pwd, config_file=CONFIG_FILE)
195
+ # setup configurations
196
+ config = {'root' => dir, 'gems' => :all}
197
+
198
+ user_config_file = config_file ? File.join(dir, config_file) : nil
199
+ user = load_config(user_config_file)
200
+
201
+ config.merge!(self.config)
202
+ config.merge!(user)
203
+
204
+ # keys must be symbolized as they are immediately
205
+ # used to initialize the Env configs
206
+ config = config.inject({}) do |options, (key, value)|
207
+ options[key.to_sym || key] = value
208
+ options
209
+ end
210
+
211
+ # instantiate
212
+ context = Context.new(:basename => config_file)
213
+ env = new(config, context)
214
+
215
+ # add the tap env if necessary
216
+ unless env.any? {|e| e.root.root == HOME }
217
+ env.push new(HOME, context)
218
+ end
219
+
220
+ env.activate
221
+ env
222
+ end
223
+
224
+ # Generates an Env for the specified gem or Gem::Specification. The
225
+ # gemspec for the gem is used to determine the env configuration in
226
+ # the following way:
227
+ #
228
+ # root: the gem path
229
+ # gems: all gem dependencies with a config_file
230
+ # const_paths: the gem require paths
231
+ # set_const_paths: false (because RubyGems sets them for you)
232
+ #
233
+ # Configurations specified in the gem config_file override these
234
+ # defaults.
235
+ def setup_gem(gem_name, context=Context.new)
236
+ spec = Gems.gemspec(gem_name)
19
237
  path = spec.full_gem_path
20
- basename = context[:basename]
21
238
 
239
+ # determine gem dependencies that have a config_file;
240
+ # these will be set as the gems for the new Env
22
241
  dependencies = []
23
242
  spec.dependencies.each do |dependency|
24
243
  unless dependency.type == :runtime
@@ -32,8 +251,8 @@ module Tap
32
251
  next
33
252
  end
34
253
 
35
- if basename && !File.exists?(File.join(gemspec.full_gem_path, basename))
36
- next
254
+ if config_file = context.config_file(gemspec.full_gem_path)
255
+ next unless File.exists?(config_file)
37
256
  end
38
257
 
39
258
  dependencies << gemspec
@@ -42,12 +261,14 @@ module Tap
42
261
  config = {
43
262
  'root' => path,
44
263
  'gems' => dependencies,
45
- 'load_paths' => spec.require_paths,
46
- 'set_load_paths' => false
264
+ 'const_paths' => spec.require_paths,
265
+ 'set_const_paths' => false
47
266
  }
48
267
 
49
- if context[:basename]
50
- config.merge!(Env.load_config(File.join(path, context[:basename])))
268
+ # override the default configs with whatever configs
269
+ # are specified in the gem config file
270
+ if config_file = context.config_file(path)
271
+ config.merge!(load_config(config_file))
51
272
  end
52
273
 
53
274
  new(config, context)
@@ -55,6 +276,8 @@ module Tap
55
276
 
56
277
  # Loads configurations from path as YAML. Returns an empty hash if the path
57
278
  # loads to nil or false (as happens for empty files), or doesn't exist.
279
+ #
280
+ # Raises a ConfigError if the configurations do not load properly.
58
281
  def load_config(path)
59
282
  return {} unless path
60
283
 
@@ -64,88 +287,42 @@ module Tap
64
287
  raise ConfigError.new($!, path)
65
288
  end
66
289
  end
67
-
68
- def scan_dir(load_path, pattern='**/*.rb')
69
- Dir.chdir(load_path) do
70
- Dir.glob(pattern).each do |require_path|
71
- next unless File.file?(require_path)
72
-
73
- default_const_name = require_path.chomp('.rb').camelize
74
-
75
- # note: the default const name has to be set here to allow for implicit
76
- # constant attributes. An error can arise if the same path is globed
77
- # from two different dirs... no surefire solution.
78
- Lazydoc[require_path].default_const_name = default_const_name
79
-
80
- # scan for constants
81
- Lazydoc::Document.scan(File.read(require_path)) do |const_name, type, comment|
82
- const_name = default_const_name if const_name.empty?
83
- constant = Constant.new(const_name, require_path, comment)
84
- yield(type, constant)
85
-
86
- ###############################################################
87
- # [depreciated] manifest as a task key will be removed at 1.0
88
- if type == 'manifest'
89
- warn "depreciation: ::task should be used instead of ::manifest as a resource key (#{require_path})"
90
- yield('task', constant)
91
- end
92
- ###############################################################
93
- end
94
- end
95
- end
96
- end
97
-
98
- def scan(path, key='[a-z_]+')
99
- Lazydoc::Document.scan(File.read(path), key) do |const_name, type, comment|
100
- if const_name.empty?
101
- unless const_name = Lazydoc[path].default_const_name
102
- raise "could not determine a constant name for #{type} in: #{path.inspect}"
103
- end
104
- end
105
-
106
- constant = Constant.new(const_name, path, comment)
107
- yield(type, constant)
108
-
109
- ###############################################################
110
- # [depreciated] manifest as a task key will be removed at 1.0
111
- if type == 'manifest'
112
- warn "depreciation: ::task should be used instead of ::manifest as a resource key (#{require_path})"
113
- yield('task', constant)
114
- end
115
- ###############################################################
116
- end
117
- end
118
290
  end
119
- self.instance = nil
120
291
 
121
- include Enumerable
122
292
  include Configurable
293
+ include Enumerable
123
294
  include Minimap
124
295
 
125
- # Matches a compound registry search key. After the match, if the key is
126
- # compound then:
127
- #
128
- # $1:: env_key
129
- # $2:: key
130
- #
131
- # If the key is not compound, $2 is nil and $1 is the key.
132
- COMPOUND_KEY = /^((?:[A-z]:(?:\/|\\))?.*?)(?::(.*))?$/
133
-
296
+ # The config file path
297
+ CONFIG_FILE = "tap.yml"
298
+
299
+ # The home directory for Tap
300
+ HOME = File.expand_path("#{File.dirname(__FILE__)}/../..")
301
+
134
302
  # An array of nested Envs, by default comprised of the env_path
135
- # + gem environments (in that order).
303
+ # + gem environments (in that order). Envs can be manually set
304
+ # to override these defaults.
136
305
  attr_reader :envs
137
306
 
307
+ # A Context tracking information shared among a set of envs.
138
308
  attr_reader :context
139
309
 
140
- attr_reader :manifests
141
-
142
310
  # The Root directory structure for self.
143
- nest(:root, Root, :set_default => false)
311
+ nest(:root, Root, :init => false)
144
312
 
145
- # Specify gems to add as nested Envs. Gems may be specified
146
- # by name and/or version, like 'gemname >= 1.2'; by default the
147
- # latest version of the gem is selected. Gems are not activated
148
- # by Env.
313
+ # Specify gems to add as nested Envs. Gems may be specified by name
314
+ # and/or version, like 'gemname >= 1.2'; by default the latest version
315
+ # of the gem is selected.
316
+ #
317
+ # Several special values also exist:
318
+ #
319
+ # :NONE, :none indicates no gems (same as nil, false)
320
+ # :LATEST the latest version of all gems
321
+ # :ALL all gems
322
+ # :latest the latest version of all gems with a config file
323
+ # :all all gems with a config file
324
+ #
325
+ # Gems are not activated by Env.
149
326
  config_attr :gems, [] do |input|
150
327
  input = yaml_load(input) if input.kind_of?(String)
151
328
 
@@ -156,9 +333,12 @@ module Tap
156
333
  # latest and all, no filter
157
334
  Gems.select_gems(input == :LATEST)
158
335
  when :latest, :all
159
- # latest and all, filtering by basename
336
+ # latest and all, filtering by the existence of a
337
+ # config file; all gems are selected if no config
338
+ # file can be determined.
160
339
  Gems.select_gems(input == :latest) do |spec|
161
- basename == nil || File.exists?(File.join(spec.full_gem_path, basename))
340
+ config_file = context.config_file(spec.full_gem_path)
341
+ config_file == nil || File.exists?(config_file)
162
342
  end
163
343
  else
164
344
  # resolve gem names manually
@@ -170,46 +350,45 @@ module Tap
170
350
  reset_envs
171
351
  end
172
352
 
173
- # Specify directories to load as nested Envs.
353
+ # Specify directories to load as nested Envs. Configurations for the
354
+ # env are loaded from the config file under dir, if it exists.
174
355
  config_attr :env_paths, [] do |input|
175
356
  @env_paths = resolve_paths(input)
176
357
  reset_envs
177
358
  end
178
359
 
179
- # Designates paths added to $LOAD_PATH on activation (see set_load_paths).
180
- # These paths are also the default directories searched for resources.
181
- config_attr :load_paths, [:lib] do |input|
182
- raise "load_paths cannot be modified once active" if active?
183
- @load_paths = resolve_paths(input)
360
+ # Designates directories searched for constants. The const_paths are
361
+ # added to $LOAD_PATH on activation if set_const_paths is specified.
362
+ config_attr :const_paths, [:lib] do |input|
363
+ raise "const_paths cannot be modified once active" if active?
364
+ @const_paths = resolve_paths(input)
184
365
  end
185
366
 
186
- # If set to true load_paths are added to $LOAD_PATH on activation.
187
- config_attr :set_load_paths, true do |input|
188
- raise "set_load_paths cannot be modified once active" if active?
189
- @set_load_paths = Configurable::Validation.boolean[input]
367
+ # If set to true const_paths are added to $LOAD_PATH on activation.
368
+ config_attr :set_const_paths, true do |input|
369
+ raise "set_const_paths cannot be modified once active" if active?
370
+ @set_const_paths = Configurable::Validation.boolean[input]
190
371
  end
191
372
 
192
- # Initializes a new Env linked to the specified directory. A config file
193
- # basename may be specified to load configurations from 'dir/basename' as
194
- # YAML. If a basename is specified, the same basename will be used to
195
- # load configurations for nested envs.
373
+ # Initializes a new Env linked to the specified directory. Configurations
374
+ # for the env will be loaded from the config file (as determined by the
375
+ # context) if it exists.
196
376
  #
197
- # Configurations may be manually provided in the place of dir. In that
198
- # case, the same rules apply for loading configurations for nested envs,
199
- # but no configurations will be loaded for the current instance.
377
+ # A configuration hash may be manually provided in the place of dir. In
378
+ # that case, no configurations will be loaded, even if the config file
379
+ # exists.
200
380
  #
201
- # The cache is used internally to prevent infinite loops of nested envs,
202
- # and to optimize the generation of manifests.
381
+ # Context can be specified as a Context, or a Hash used to initialize a
382
+ # Context.
203
383
  def initialize(config_or_dir=Dir.pwd, context={})
204
- @active = false
205
- @manifests = {}
206
- @context = context
207
384
 
208
385
  # setup root
209
386
  config = nil
210
387
  @root = case config_or_dir
211
- when Root then config_or_dir
212
- when String then Root.new(config_or_dir)
388
+ when Root
389
+ config_or_dir
390
+ when String
391
+ Root.new(config_or_dir)
213
392
  else
214
393
  config = config_or_dir
215
394
 
@@ -221,24 +400,28 @@ module Tap
221
400
  root.kind_of?(Root) ? root : Root.new(root)
222
401
  end
223
402
 
224
- if basename && !config
225
- config = Env.load_config(File.join(@root.root, basename))
226
- end
403
+ # note registration requires root.root, and so the
404
+ # setup of context must follow the setup of root.
405
+ @context = case context
406
+ when Context
407
+ context
408
+ when Hash
409
+ Context.new(context)
410
+ else raise "cannot convert #{context.inspect} to Tap::Env::Context"
411
+ end
412
+ @context.register(self)
227
413
 
228
- if instance(@root.root)
229
- raise "context already has an env for: #{@root.root}"
230
- end
231
- instances << self
232
-
233
- # set these for reset_env
414
+ # these need to be set for reset_env
415
+ @active = false
234
416
  @gems = nil
235
417
  @env_paths = nil
236
- initialize_config(config || {})
237
- end
238
-
239
- # The minikey for self (root.root).
240
- def minikey
241
- root.root
418
+
419
+ # only load configurations if configs were not provided
420
+ config ||= Env.load_config(@context.config_file(@root.root))
421
+ initialize_config(config)
422
+
423
+ # set the invert flag
424
+ @invert = false
242
425
  end
243
426
 
244
427
  # Sets envs removing duplicates and instances of self. Setting envs
@@ -265,6 +448,7 @@ module Tap
265
448
  end
266
449
  self
267
450
  end
451
+ alias_method :<<, :push
268
452
 
269
453
  # Passes each nested env to the block in order, starting with self.
270
454
  def each
@@ -306,33 +490,29 @@ module Tap
306
490
 
307
491
  # Activates self by doing the following, in order:
308
492
  #
309
- # * sets Env.instance to self (unless already set)
310
493
  # * activate nested environments
311
- # * unshift load_paths to $LOAD_PATH (if set_load_paths is true)
494
+ # * unshift const_paths to $LOAD_PATH (if set_const_paths is true)
312
495
  #
313
- # Once active, the current envs and load_paths are frozen and cannot be
496
+ # Once active, the current envs and const_paths are frozen and cannot be
314
497
  # modified until deactivated. Returns true if activate succeeded, or
315
498
  # false if self is already active.
316
499
  def activate
317
500
  return false if active?
318
501
 
319
502
  @active = true
320
- unless self.class.instance(false)
321
- self.class.instance = self
322
- end
323
503
 
324
- # freeze envs and load paths
504
+ # freeze envs and const paths
325
505
  @envs.freeze
326
- @load_paths.freeze
506
+ @const_paths.freeze
327
507
 
328
508
  # activate nested envs
329
509
  envs.reverse_each do |env|
330
510
  env.activate
331
511
  end
332
512
 
333
- # add load paths
334
- if set_load_paths
335
- load_paths.reverse_each do |path|
513
+ # add const paths
514
+ if set_const_paths
515
+ const_paths.reverse_each do |path|
336
516
  $LOAD_PATH.unshift(path)
337
517
  end
338
518
 
@@ -345,12 +525,19 @@ module Tap
345
525
  # Deactivates self by doing the following in order:
346
526
  #
347
527
  # * deactivates nested environments
348
- # * removes load_paths from $LOAD_PATH (if set_load_paths is true)
349
- # * sets Env.instance to nil (if set to self)
350
- # * clears cached manifest data
528
+ # * removes const_paths from $LOAD_PATH (if set_const_paths is true)
351
529
  #
352
- # Once deactivated, envs and load_paths are unfrozen and may be modified.
530
+ # Once deactivated, envs and const_paths are unfrozen and may be modified.
353
531
  # Returns true if deactivate succeeded, or false if self is not active.
532
+ #
533
+ # ==== Note
534
+ #
535
+ # Deactivation does not necessarily leave $LOAD_PATH in the same condition
536
+ # as before activation. A pre-existing $LOAD_PATH entry can go missing if
537
+ # it is also registered as an env load_path (deactivation doesn't know to
538
+ # leave such paths alone).
539
+ #
540
+ # Deactivation, like constant unloading should be done with caution.
354
541
  def deactivate
355
542
  return false unless active?
356
543
  @active = false
@@ -360,20 +547,14 @@ module Tap
360
547
  env.deactivate
361
548
  end
362
549
 
363
- # remove load paths
364
- load_paths.each do |path|
550
+ # remove const paths
551
+ const_paths.each do |path|
365
552
  $LOAD_PATH.delete(path)
366
- end if set_load_paths
553
+ end if set_const_paths
367
554
 
368
- # unfreeze envs and load paths
555
+ # unfreeze envs and const paths
369
556
  @envs = @envs.dup
370
- @load_paths = @load_paths.dup
371
-
372
- # clear cached data
373
- klass = self.class
374
- if klass.instance(false) == self
375
- klass.instance = nil
376
- end
557
+ @const_paths = @const_paths.dup
377
558
 
378
559
  true
379
560
  end
@@ -383,6 +564,16 @@ module Tap
383
564
  @active
384
565
  end
385
566
 
567
+ # Globs the abstract directory for files in the specified directory alias,
568
+ # matching the pattern. The expanded path of each matching file is
569
+ # returned.
570
+ def glob(dir, pattern="**/*")
571
+ hlob(dir, pattern).values.sort!
572
+ end
573
+
574
+ # Same as glob but returns results as a hash of (relative_path, path)
575
+ # pairs. In short the hash defines matching files in the abstract
576
+ # directory, linked to the actual path for these files.
386
577
  def hlob(dir, pattern="**/*")
387
578
  results = {}
388
579
  each do |env|
@@ -395,11 +586,11 @@ module Tap
395
586
  results
396
587
  end
397
588
 
398
- def glob(dir, pattern="**/*")
399
- hlob(dir, pattern).values.sort!
400
- end
401
-
402
- def path(dir, *paths)
589
+ # Returns the path to the specified file, as determined by root.
590
+ #
591
+ # If a block is given, a path for each env will be yielded until the block
592
+ # returns a true value. Returns nil if the block never returns true.
593
+ def path(dir = :root, *paths)
403
594
  each do |env|
404
595
  path = env.root.path(dir, *paths)
405
596
  return path if !block_given? || yield(path)
@@ -413,13 +604,12 @@ module Tap
413
604
  #
414
605
  # path(dir, module_path, *paths)
415
606
  #
416
- # By default, 'module_path' is 'module.to_s.underscore', but modules can
607
+ # By default 'module_path' is 'module.to_s.underscore' but modules can
417
608
  # specify an alternative by providing a module_path method.
418
609
  #
419
- # The paths are yielded to the block and when the block returns true,
420
- # the path will be returned. If no block is given, the first module path
421
- # is returned. Returns nil if the block never returns true.
422
- #
610
+ # Paths are yielded to the block until the block returns true, at which
611
+ # point the current the path is returned. If no block is given, the
612
+ # first path is returned. Returns nil if the block never returns true.
423
613
  def module_path(dir, modules, *paths, &block)
424
614
  paths.compact!
425
615
  while current = modules.shift
@@ -446,111 +636,119 @@ module Tap
446
636
  module_path(dir, superclasses, *paths, &block)
447
637
  end
448
638
 
449
- def registry(build=false)
450
- builders.each_pair do |type, builder|
451
- registry[type] ||= builder.call(self)
452
- end if build
453
-
454
- registries[minikey] ||= begin
455
- registry = {}
456
- load_paths.each do |load_path|
639
+ # Generates a Manifest for self using the block as a builder. The builder
640
+ # receives an env and should return an array of resources, each of which
641
+ # can be minimappped. Minimapping requires that the resource is either
642
+ # a path string, or provides a 'path' method that returns a path string.
643
+ # Alternatively, a Minimap may be returned.
644
+ #
645
+ # If a type is specified, then the manifest cache will be linked to the
646
+ # context cache.
647
+ def manifest(type=nil, &block) # :yields: env
648
+ cache = type ? (context.cache[type] ||= {}) : {}
649
+ Manifest.new(self, block, cache)
650
+ end
651
+
652
+ # Returns a manifest of Constants located in .rb files under each of the
653
+ # const_paths. Constants are identified using Lazydoc constant attributes;
654
+ # all attributes are registered to the constants for classification
655
+ # (for example as a task, join, etc).
656
+ def constants
657
+ @constants ||= manifest(:constants) do |env|
658
+ constants = {}
659
+
660
+ env.const_paths.each do |load_path|
457
661
  next unless File.directory?(load_path)
458
-
459
- Env.scan_dir(load_path) do |type, constant|
460
- (registry[type.to_sym] ||= []) << constant
461
- end
662
+ Constant.scan(load_path, "**/*.rb", constants)
663
+ end
664
+
665
+ constants.keys.sort!.collect! do |key|
666
+ constants[key]
462
667
  end
463
-
464
- registry
465
- end
466
- end
467
-
468
- def register(type, override=false, &block)
469
- type = type.to_sym
470
-
471
- # error for existing, or overwrite
472
- case
473
- when override
474
- builders.delete(type)
475
- registries.each {|root, registry| registry.delete(type) }
476
- when builders.has_key?(type)
477
- raise "a builder is already registered for: #{type.inspect}"
478
- when registries.any? {|root, registry| registry.has_key?(type) }
479
- raise "entries are already registered for: #{type.inspect}"
480
668
  end
481
-
482
- builders[type] = block
483
669
  end
484
670
 
485
- #--
486
- # Potential bug, constants can be added twice.
487
- def scan(path, key='[a-z_]+')
488
- registry = self.registry
489
- Env.scan(path, key) do |type, constant|
490
- (registry[type.to_sym] ||= []) << constant
671
+ # Seeks a constant for the key, constantizing if necessary. If invert is
672
+ # true, this method seeks and returns a key for the input constant. In
673
+ # both cases AGET returns nil if no key-constant pair can be found.
674
+ def [](key, invert=invert?)
675
+ if invert
676
+ const_name = key.to_s
677
+ constants.unseek(true) do |const|
678
+ const_name == const.const_name
679
+ end
680
+ else
681
+ if constant = constants.seek(key)
682
+ constant.constantize
683
+ else
684
+ nil
685
+ end
491
686
  end
492
687
  end
493
688
 
494
- def manifest(type) # :yields: env
495
- type = type.to_sym
496
-
497
- registry[type] ||= begin
498
- builder = builders[type]
499
- builder ? builder.call(self) : []
500
- end
501
-
502
- manifests[type] ||= Manifest.new(self, type)
689
+ # Indicates AGET looks up constants from keys (false), or keys from
690
+ # constants (true).
691
+ def invert?
692
+ @invert
503
693
  end
504
694
 
505
- def [](type)
506
- manifest(type)
695
+ # Inverts the AGET lookup for self.
696
+ def invert!
697
+ @invert = !@invert
698
+ self
507
699
  end
508
700
 
509
- def reset
510
- manifests.clear
511
- registries.clear
701
+ # Returns a duplicate of self with inverted AGET lookup.
702
+ def invert
703
+ dup.invert!
512
704
  end
513
705
 
514
- # Searches across each for the first registered object minimatching key. A
515
- # single env can be specified by using a compound key like 'env_key:key'.
516
- #
517
- # Returns nil if no matching object is found.
518
- def seek(type, key, value_only=true)
519
- key =~ COMPOUND_KEY
520
- envs = if $2
521
- # compound key, match for env
522
- key = $2
523
- [minimatch($1)].compact
524
- else
525
- # not a compound key, search all envs by iterating self
526
- self
527
- end
528
-
529
- # traverse envs looking for the first
530
- # manifest entry matching key
531
- envs.each do |env|
532
- if value = env.manifest(type).minimatch(key)
533
- return value_only ? value : [env, value]
534
- end
535
- end
536
-
537
- nil
538
- end
539
-
540
- def reverse_seek(type, key_only=true, &block)
541
- each do |env|
542
- manifest = env.manifest(type)
543
- if value = manifest.find(&block)
544
- key = manifest.minihash(true)[value]
545
- return key_only ? key : "#{minihash(true)[env]}:#{key}"
546
- end
706
+ # Scans the files matched under the directory and pattern for constants
707
+ # and adds them to the existing constants for self.
708
+ def scan(dir, pattern="**/*.rb")
709
+ new_entries = {}
710
+ entries = constants.entries(self)
711
+ entries.each {|const| new_entries[const.const_name] = const }
712
+
713
+ Constant.scan(root[dir], pattern, new_entries)
714
+
715
+ entries.replace(new_entries.keys)
716
+ entries.sort!.collect! {|key| new_entries[key] }
717
+ entries
718
+ end
719
+
720
+ # Registers a constant with self. The constant is stored as a new
721
+ # Constant in the constants manifest. Returns the new Constant.
722
+ # If the constant is already registered, the existing Constant is
723
+ # returned.
724
+ def register(constant)
725
+ const_name = constant.to_s
726
+ entries = constants.entries(self)
727
+
728
+ # try to find the existing Constant before making a new constant
729
+ unless constant = entries.find {|const| const.const_name == const_name}
730
+ constant = Constant.new(const_name)
731
+ entries << constant
732
+ entries.replace entries.sort_by {|const| const.const_name }
547
733
  end
548
-
549
- nil
734
+
735
+ constant
550
736
  end
551
737
 
552
- # All templaters are yielded to the block before any are built. This
553
- # allows globals to be determined for all environments.
738
+ # When no template is specified, inspect generates a fairly standard
739
+ # inspection string. When a template is provided, inspect builds a
740
+ # Templater for each env with the following local variables:
741
+ #
742
+ # variable value
743
+ # env the current env
744
+ # env_keys a minihash for all envs
745
+ #
746
+ # If a block is given, the globals and templater are yielded before
747
+ # any templater is built; this allows each env to add env-specific
748
+ # variables. After this preparation, each templater is built with
749
+ # the globals and the results concatenated.
750
+ #
751
+ # The template is built with filename, if specified (for debugging).
554
752
  def inspect(template=nil, globals={}, filename=nil) # :yields: templater, globals
555
753
  if template == nil
556
754
  return "#<#{self.class}:#{object_id} root='#{root.root}'>"
@@ -565,27 +763,13 @@ module Tap
565
763
  templater.build(globals, filename)
566
764
  end.join
567
765
  end
568
-
766
+
569
767
  protected
570
768
 
571
- def registries # :nodoc:
572
- context[:registries] ||= {}
573
- end
574
-
575
- def basename # :nodoc:
576
- context[:basename]
577
- end
578
-
579
- def builders # :nodoc:
580
- context[:builders] ||= {}
581
- end
582
-
583
- def instances # :nodoc:
584
- context[:instances] ||= []
585
- end
586
-
587
- def instance(path) # :nodoc:
588
- instances.find {|env| env.root.root == path }
769
+ # helper for Minimap; note that specifying env.root.root via path
770
+ # is not possible because path is required for other purposes.
771
+ def entry_to_path(env) # :nodoc:
772
+ env.root.root
589
773
  end
590
774
 
591
775
  # resets envs using the current env_paths and gems. does nothing
@@ -593,15 +777,15 @@ module Tap
593
777
  def reset_envs # :nodoc:
594
778
  if env_paths && gems
595
779
  self.envs = env_paths.collect do |path|
596
- instance(path) || Env.new(path, context)
780
+ context.instance(path) || Env.new(path, context)
597
781
  end + gems.collect do |spec|
598
- instance(spec.full_gem_path) || Env.from_gemspec(spec, context)
782
+ context.instance(spec.full_gem_path) || Env.setup_gem(spec, context)
599
783
  end
600
784
  end
601
785
  end
602
786
 
603
- # arrayifies, compacts, and resolves input paths using root, and
604
- # removes duplicates. in short:
787
+ # arrayifies, compacts, and resolves input paths using root.
788
+ # also removes duplicates. in short:
605
789
  #
606
790
  # resolve_paths ['lib', nil, 'lib', 'alt] # => [root['lib'], root['alt']]
607
791
  #