tap 0.17.1 → 0.18.0

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