tap 0.18.0 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  #