tap 0.17.1 → 0.18.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,56 @@
1
+ require 'configurable'
2
+
3
+ module Tap
4
+ class Middleware
5
+ class << self
6
+
7
+ # Instantiates an instance of self and causes app to use the instance
8
+ # as middleware.
9
+ def parse(argv=ARGV, app=Tap::App.instance)
10
+ parse!(argv.dup, app)
11
+ end
12
+
13
+ # Same as parse, but removes arguments destructively.
14
+ def parse!(argv=ARGV, app=Tap::App.instance)
15
+ opts = ConfigParser.new
16
+ opts.separator "configurations:"
17
+ opts.add(configurations)
18
+
19
+ args = opts.parse!(argv, :add_defaults => false)
20
+ instantiate({:config => opts.nested_config}, app)
21
+ end
22
+
23
+ # Instantiates an instance of self and causes app to use the instance
24
+ # as middleware.
25
+ def instantiate(argh, app=Tap::App.instance)
26
+ app.use(self, argh[:config] || {})
27
+ end
28
+ end
29
+
30
+ include Configurable
31
+
32
+ # The call stack.
33
+ attr_reader :stack
34
+
35
+ def initialize(stack, config={})
36
+ @stack = stack
37
+ initialize_config(config)
38
+ end
39
+
40
+ # Returns the app at the base of the stack.
41
+ def app
42
+ @app ||= begin
43
+ current = stack
44
+ until current.kind_of?(App::Stack)
45
+ current = current.stack
46
+ end
47
+ current.app
48
+ end
49
+ end
50
+
51
+ # By default call simply calls stack with the node and inputs.
52
+ def call(node, inputs=[])
53
+ stack.call(node, inputs)
54
+ end
55
+ end
56
+ end
data/lib/tap/schema.rb CHANGED
@@ -1,11 +1,63 @@
1
+ require 'tap/app'
1
2
  require 'tap/schema/utils'
2
3
  require 'tap/schema/parser'
3
4
 
4
5
  module Tap
6
+ class App
7
+ def build(schema, options={})
8
+ options = {
9
+ :clean => true,
10
+ :validate => true
11
+ }.merge(options)
12
+
13
+ unless schema.kind_of?(Schema)
14
+ schema = Schema.new(schema)
15
+ end
16
+
17
+ if resources = options[:resources]
18
+ schema.resolve! do |type, id|
19
+ resources[type][id]
20
+ end
21
+ end
22
+
23
+ if options[:clean]
24
+ reset
25
+ end
26
+
27
+ schema.build!(self, options[:validate])
28
+ end
29
+
30
+ def to_schema
31
+ schema = Schema.new
32
+ queue.to_a.each do |task, inputs|
33
+ schema.add(task, inputs)
34
+ end
35
+
36
+ middleware.reverse_each do |m|
37
+ schema.use(m)
38
+ end
39
+
40
+ index = 0
41
+ schema.tasks.keys.each do |task|
42
+ schema.rename(task, index)
43
+ index += 1
44
+ end
45
+
46
+ if block_given?
47
+ schema.resources.each_pair do |type, resource|
48
+ yield(type, resource)
49
+ end
50
+ end
51
+
52
+ schema
53
+ end
54
+ end
55
+
5
56
  class Schema
6
57
  class << self
7
58
  def load(str)
8
- new(YAML.load(str) || {})
59
+ schema = YAML.load(str)
60
+ new(schema ? Utils.symbolize(schema) : {})
9
61
  end
10
62
 
11
63
  def load_file(path)
@@ -51,11 +103,86 @@ module Tap
51
103
  # An array of middleware to build onto the app.
52
104
  attr_reader :middleware
53
105
 
106
+ # The app used to build self
107
+ attr_reader :app
108
+
54
109
  def initialize(schema={})
55
- @tasks = schema['tasks'] || {}
56
- @joins = schema['joins'] || []
57
- @queue = schema['queue'] || []
58
- @middleware = schema['middleware'] || []
110
+ @tasks = schema[:tasks] || {}
111
+ @joins = schema[:joins] || []
112
+ @queue = schema[:queue] || []
113
+ @middleware = schema[:middleware] || []
114
+
115
+ @app = nil
116
+ end
117
+
118
+ def add(node, inputs=nil)
119
+ collect_tasks(node).collect do |task|
120
+ tasks[task] = task.to_hash
121
+ task.joins
122
+ end.flatten.uniq.each do |join|
123
+ joins << [join.inputs, join.outputs, join.to_hash]
124
+ end
125
+
126
+ if inputs
127
+ queue << [node, inputs]
128
+ end
129
+
130
+ self
131
+ end
132
+
133
+ def use(middleware)
134
+ self.middleware << middleware.to_hash
135
+ self
136
+ end
137
+
138
+ def resources
139
+ {
140
+ :task => tasks.values,
141
+ :join => joins.collect {|join| join[2] },
142
+ :middleware => middleware
143
+ }
144
+ end
145
+
146
+ # Renames the current_key task to new_key. References in joins and
147
+ # queue are updated by rename. Raises an error if built? or if the
148
+ # specified task does not exist.
149
+ def rename(current_key, new_key)
150
+ if built?
151
+ raise "cannot rename if built"
152
+ end
153
+
154
+ # rename task
155
+ unless task = tasks.delete(current_key)
156
+ raise "unknown task: #{current_key.inspect}"
157
+ end
158
+ tasks[new_key] = task
159
+
160
+ # update join references
161
+ joins.each do |inputs, outputs, join|
162
+ inputs.each_index do |index|
163
+ inputs[index] = new_key if inputs[index] == current_key
164
+ end
165
+
166
+ outputs.each_index do |index|
167
+ outputs[index] = new_key if outputs[index] == current_key
168
+ end
169
+ end
170
+
171
+ # update queue references, note both array and
172
+ # reference-style entries must be handled
173
+ queue.each_index do |index|
174
+ if queue[index].kind_of?(Array)
175
+ if queue[index][0] == current_key
176
+ queue[index][0] = new_key
177
+ end
178
+ else
179
+ if queue[index] == current_key
180
+ queue[index] = new_key
181
+ end
182
+ end
183
+ end
184
+
185
+ self
59
186
  end
60
187
 
61
188
  def resolve!
@@ -91,13 +218,13 @@ module Tap
91
218
  errors = []
92
219
  tasks.each_value do |task|
93
220
  unless resolved?(task)
94
- errors << "unknown task: #{task.inspect}"
221
+ errors << "unresolvable task: #{task.inspect}"
95
222
  end
96
223
  end
97
224
 
98
225
  joins.each do |inputs, outputs, join|
99
226
  unless resolved?(join)
100
- errors << "unknown join: #{join.inspect}"
227
+ errors << "unresolvable join: #{join.inspect}"
101
228
  end
102
229
 
103
230
  inputs.each do |key|
@@ -125,7 +252,7 @@ module Tap
125
252
 
126
253
  middleware.each do |m|
127
254
  unless resolved?(m)
128
- errors << "unknown middleware: #{m}"
255
+ errors << "unresolvable middleware: #{m.inspect}"
129
256
  end
130
257
  end
131
258
 
@@ -163,11 +290,14 @@ module Tap
163
290
  self
164
291
  end
165
292
 
166
- def build!(app)
293
+ def build!(app, validate=true)
294
+ validate! if validate
295
+
167
296
  # instantiate tasks
168
297
  tasks.each_pair do |key, task|
169
298
  tasks[key] = instantiate(task, app)
170
299
  end
300
+ tasks.freeze
171
301
 
172
302
  # build the workflow
173
303
  joins.collect! do |inputs, outputs, join|
@@ -175,26 +305,47 @@ module Tap
175
305
  outputs = outputs.collect {|key| tasks[key] }
176
306
  instantiate(join, app).join(inputs, outputs)
177
307
  end
308
+ joins.freeze
178
309
 
179
310
  # utilize middleware
180
311
  middleware.collect! do |middleware|
181
312
  instantiate(middleware, app)
182
313
  end
314
+ middleware.freeze
183
315
 
184
316
  # enque tasks
185
317
  queue.each do |(key, inputs)|
186
318
  app.enq(tasks[key], *inputs)
187
319
  end
320
+ queue.clear.freeze
321
+
322
+ @app = app
323
+ self
324
+ end
325
+
326
+ def built?
327
+ @app != nil
328
+ end
329
+
330
+ def enque(key, *inputs)
331
+ unless built?
332
+ raise "cannot enque unless built"
333
+ end
334
+
335
+ unless task = tasks[key]
336
+ raise "unknown task: #{key.inspect}"
337
+ end
188
338
 
189
- tasks
339
+ app.queue.enq(task, inputs)
340
+ task
190
341
  end
191
342
 
192
343
  # Creates an hash dump of self.
193
344
  def to_hash
194
- { 'tasks' => tasks,
195
- 'joins' => joins,
196
- 'queue' => queue,
197
- 'middleware' => middleware
345
+ { :tasks => tasks,
346
+ :joins => joins,
347
+ :queue => queue,
348
+ :middleware => middleware
198
349
  }
199
350
  end
200
351
 
@@ -202,5 +353,22 @@ module Tap
202
353
  def dump(io=nil)
203
354
  YAML.dump(to_hash, io)
204
355
  end
356
+
357
+ protected
358
+
359
+ # helper to collect all tasks and tasks joined to task
360
+ def collect_tasks(task, collection=[]) # :nodoc:
361
+ unless collection.include?(task)
362
+ collection << task
363
+
364
+ task.joins.each do |join|
365
+ (join.inputs + join.outputs).each do |input|
366
+ collect_tasks(input, collection)
367
+ end
368
+ end
369
+ end
370
+
371
+ collection
372
+ end
205
373
  end
206
374
  end
@@ -1,10 +1,11 @@
1
1
  module Tap
2
2
  class Schema
3
3
  module Utils
4
+ module_function
4
5
 
5
6
  def instantiate(data, app)
6
7
  case data
7
- when Hash then data['class'].instantiate(symbolize(data), app)
8
+ when Hash then data[:class].instantiate(data, app)
8
9
  when Array then data.shift.parse!(data, app)
9
10
  else raise "cannot instantiate: #{data.inspect}"
10
11
  end
@@ -12,7 +13,7 @@ module Tap
12
13
 
13
14
  def resolved?(data)
14
15
  case data
15
- when Hash then data['class'].respond_to?(:instantiate)
16
+ when Hash then data[:class].respond_to?(:instantiate)
16
17
  when Array then data[0].respond_to?(:parse!)
17
18
  else false
18
19
  end
@@ -24,7 +25,8 @@ module Tap
24
25
  case data
25
26
  when Hash
26
27
  unless resolved?(data)
27
- data['class'] = yield(data['id']) || data['id']
28
+ data = symbolize(data)
29
+ data[:class] = yield(data[:id]) || data[:id]
28
30
  end
29
31
  when Array
30
32
  data[0] = yield(data[0]) || data[0]
data/lib/tap/task.rb CHANGED
@@ -3,14 +3,28 @@ require 'tap/root'
3
3
  require 'tap/env/string_ext'
4
4
 
5
5
  module Tap
6
- module Support
7
- autoload(:Templater, 'tap/support/templater')
8
- end
9
-
10
6
  class App
11
- # Generates a task initialized to self.
7
+ # Generates a task with the specified config, initialized to self.
8
+ #
9
+ # A block may be provided to overrride the process method; it will be
10
+ # called with the task instance, plus any inputs.
11
+ #
12
+ # no_inputs = app.task {|task| [] }
13
+ # one_input = app.task {|task, input| [input] }
14
+ # mixed_inputs = app.task {|task, a, b, *args| [a, b, args] }
15
+ #
16
+ # no_inputs.execute # => []
17
+ # one_input.execute(:a) # => [:a]
18
+ # mixed_inputs.execute(:a, :b) # => [:a, :b, []]
19
+ # mixed_inputs.execute(:a, :b, 1, 2, 3) # => [:a, :b, [1,2,3]]
20
+ #
12
21
  def task(config={}, klass=Task, &block)
13
- klass.intern(config, self, &block)
22
+ instance = klass.new(config, self)
23
+ if block_given?
24
+ instance.extend Intern
25
+ instance.process_block = block
26
+ end
27
+ instance
14
28
  end
15
29
  end
16
30
 
@@ -40,18 +54,6 @@ module Tap
40
54
  # MixedInputs.new.execute(:a, :b) # => [:a, :b, []]
41
55
  # MixedInputs.new.execute(:a, :b, 1, 2, 3) # => [:a, :b, [1,2,3]]
42
56
  #
43
- # Tasks may be created with new, or with intern. Intern overrides process
44
- # using a block that receives the task and the inputs.
45
- #
46
- # no_inputs = Task.intern {|task| [] }
47
- # one_input = Task.intern {|task, input| [input] }
48
- # mixed_inputs = Task.intern {|task, a, b, *args| [a, b, args] }
49
- #
50
- # no_inputs.execute # => []
51
- # one_input.execute(:a) # => [:a]
52
- # mixed_inputs.execute(:a, :b) # => [:a, :b, []]
53
- # mixed_inputs.execute(:a, :b, 1, 2, 3) # => [:a, :b, [1,2,3]]
54
- #
55
57
  # === Configuration
56
58
  #
57
59
  # Tasks are configurable. By default each task will be configured as
@@ -124,47 +126,31 @@ module Tap
124
126
  include Configurable
125
127
 
126
128
  class << self
127
- # Returns class dependencies
128
- attr_reader :dependencies
129
-
130
- # Returns or initializes the instance of self cached with app.
131
- def instance(app=Tap::App.instance, auto_initialize=true)
132
- app.cache[self] ||= (auto_initialize ? new({}, app) : nil)
133
- end
134
-
135
129
  def inherited(child) # :nodoc:
136
130
  unless child.instance_variable_defined?(:@source_file)
137
131
  caller[0] =~ Lazydoc::CALLER_REGEXP
138
132
  child.instance_variable_set(:@source_file, File.expand_path($1))
139
133
  end
140
-
141
- child.instance_variable_set(:@dependencies, dependencies.dup)
142
134
  super
143
135
  end
144
136
 
145
- # Instantiates a new task with the input arguments and overrides
146
- # process with the block. The block will be called with the task
147
- # instance, plus any inputs.
148
- #
149
- # Simply instantiates a new task if no block is given.
150
- def intern(config={}, app=Tap::App.instance, &block) # :yields: task, inputs...
151
- instance = new(config, app)
152
- if block_given?
153
- instance.extend Support::Intern(:process)
154
- instance.process_block = block
155
- end
156
- instance
157
- end
158
-
159
137
  # Parses the argv into an instance of self. By default parse
160
138
  # parses an argh then calls instantiate, but there is no requirement
161
139
  # that this occurs in subclasses.
162
- def parse(argv=ARGV, app=Tap::App.instance)
163
- parse!(argv.dup, app)
140
+ #
141
+ # ==== Block Overrides
142
+ #
143
+ # For convenience, parse will yield the internal ConfigParser to the
144
+ # block, if given. This functionality was added to Task so they are
145
+ # more flexible in executable files but it is not a part of the API
146
+ # requirements for parse/parse!.
147
+ #
148
+ def parse(argv=ARGV, app=Tap::App.instance, &block) # :yields: opts
149
+ parse!(argv.dup, app, &block)
164
150
  end
165
151
 
166
152
  # Same as parse, but removes arguments destructively.
167
- def parse!(argv=ARGV, app=Tap::App.instance)
153
+ def parse!(argv=ARGV, app=Tap::App.instance) # :yields: opts
168
154
  opts = ConfigParser.new
169
155
 
170
156
  unless configurations.empty?
@@ -177,12 +163,17 @@ module Tap
177
163
 
178
164
  # add option to print help
179
165
  opts.on("--help", "Print this help") do
180
- prg = case $0
181
- when /rap$/ then 'rap'
182
- else 'tap run --'
166
+ lines = desc.kind_of?(Lazydoc::Comment) ? desc.wrap(77, 2, nil) : []
167
+ lines.collect! {|line| " #{line}"}
168
+ unless lines.empty?
169
+ line = '-' * 80
170
+ lines.unshift(line)
171
+ lines.push(line)
183
172
  end
184
-
185
- puts "#{help}usage: #{prg} #{to_s.underscore} #{args}"
173
+
174
+ puts "#{self}#{desc.empty? ? '' : ' -- '}#{desc.to_s}"
175
+ puts lines.join("\n")
176
+ puts "usage: tap run -- #{to_s.underscore} #{args}"
186
177
  puts
187
178
  puts opts
188
179
  exit
@@ -193,6 +184,8 @@ module Tap
193
184
  opts.config.merge!(load_config(config_file))
194
185
  end
195
186
 
187
+ yield(opts) if block_given?
188
+
196
189
  # (note defaults are not added so they will not
197
190
  # conflict with string keys from a config file)
198
191
  argv = opts.parse!(argv, :add_defaults => false)
@@ -200,45 +193,13 @@ module Tap
200
193
  instantiate({:config => opts.nested_config}, app)
201
194
  end
202
195
 
203
- # Instantiates an instance of self and returns an instance of self and
204
- # an array of arguments (implicitly to be enqued to the instance).
196
+ # Instantiates an instance of self and returns an instance of self.
205
197
  def instantiate(argh={}, app=Tap::App.instance)
206
- config = argh[:config] || {}
207
- instance = new(config, app)
208
-
209
- if argh[:cache]
210
- if app.cache.has_key?(self) && app.cache[self] != instance
211
- raise "cache already has an instance for: #{self}"
212
- end
213
-
214
- app.cache[self] = instance
215
- end
216
-
217
- instance
218
- end
219
-
220
- DEFAULT_HELP_TEMPLATE = %Q{<% desc = task_class::desc %>
221
- <%= task_class %><%= desc.empty? ? '' : ' -- ' %><%= desc.to_s %>
222
-
223
- <% desc = desc.kind_of?(Lazydoc::Comment) ? desc.wrap(77, 2, nil) : [] %>
224
- <% unless desc.empty? %>
225
- <%= '-' * 80 %>
226
-
227
- <% desc.each do |line| %>
228
- <%= line %>
229
- <% end %>
230
- <%= '-' * 80 %>
231
- <% end %>
232
-
233
- }
234
-
235
- # Returns the class help.
236
- def help
237
- Tap::Support::Templater.new(DEFAULT_HELP_TEMPLATE, :task_class => self).build
198
+ new(argh[:config] || {}, app)
238
199
  end
239
200
 
240
201
  # Recursively loads path into a nested configuration file.
241
- def load_config(path)
202
+ def load_config(path) # :nodoc:
242
203
  # optimization to check for trivial paths
243
204
  return {} if Root::Utils.trivial?(path)
244
205
 
@@ -249,44 +210,6 @@ module Tap
249
210
 
250
211
  protected
251
212
 
252
- # Sets a class-level dependency; when task class B depends_on another
253
- # task class A, instances of B are initialized to depend on a shared
254
- # instance of A. The shared instance is specific to an app and can
255
- # be accessed through instance(app).
256
- #
257
- # If a non-nil name is specified, depends_on will create a reader of
258
- # the dependency instance.
259
- #
260
- # class A < Tap::Task
261
- # end
262
- #
263
- # class B < Tap::Task
264
- # depends_on :a, A
265
- # end
266
- #
267
- # app = Tap::App.new
268
- # b = B.new({}, app)
269
- # b.dependencies # => [A.instance(app)]
270
- # b.a # => A.instance(app)
271
- #
272
- # Returns self.
273
- def depends_on(name, dependency_class)
274
- unless dependencies.include?(dependency_class)
275
- dependencies << dependency_class
276
- end
277
-
278
- if name
279
- # returns the resolved result of the dependency
280
- define_method(name) do
281
- dependency_class.instance(app)
282
- end
283
-
284
- public(name)
285
- end
286
-
287
- self
288
- end
289
-
290
213
  # Defines a task subclass with the specified configurations and process
291
214
  # block. During initialization the subclass is instantiated and made
292
215
  # accessible through the name method.
@@ -379,7 +302,6 @@ module Tap
379
302
  end
380
303
 
381
304
  instance_variable_set(:@source_file, __FILE__)
382
- instance_variable_set(:@dependencies, [])
383
305
 
384
306
  lazy_attr :desc, 'task'
385
307
  lazy_attr :args, :process
@@ -407,15 +329,9 @@ module Tap
407
329
  def initialize(config={}, app=Tap::App.instance)
408
330
  @app = app
409
331
  @joins = []
410
- @dependencies = []
411
332
 
412
333
  # initialize configs
413
334
  initialize_config(config)
414
-
415
- # setup class dependencies
416
- self.class.dependencies.each do |dependency_class|
417
- depends_on dependency_class.instance(app)
418
- end
419
335
  end
420
336
 
421
337
  # Auditing method call. Resolves dependencies, executes method_name,
@@ -518,5 +434,12 @@ module Tap
518
434
  def inspect
519
435
  "#<#{self.class.to_s}:#{object_id} #{config.to_hash.inspect} >"
520
436
  end
437
+
438
+ def to_hash
439
+ {
440
+ :class => self.class,
441
+ :config => config.to_hash
442
+ }
443
+ end
521
444
  end
522
445
  end