tap 0.19.0 → 1.3.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.
Files changed (66) hide show
  1. data/History +100 -45
  2. data/MIT-LICENSE +1 -1
  3. data/README +95 -51
  4. data/bin/tap +11 -57
  5. data/bin/tapexe +84 -0
  6. data/doc/API +91 -139
  7. data/doc/Configuration +93 -0
  8. data/doc/Examples/Command Line +10 -42
  9. data/doc/Examples/Tapfile +124 -0
  10. data/doc/Ruby to Ruby +87 -0
  11. data/doc/Workflow Syntax +185 -0
  12. data/lib/tap.rb +74 -5
  13. data/lib/tap/app.rb +217 -310
  14. data/lib/tap/app/api.rb +44 -23
  15. data/lib/tap/app/queue.rb +11 -12
  16. data/lib/tap/app/stack.rb +4 -4
  17. data/lib/tap/declarations.rb +200 -0
  18. data/lib/tap/declarations/context.rb +31 -0
  19. data/lib/tap/declarations/description.rb +33 -0
  20. data/lib/tap/env.rb +133 -779
  21. data/lib/tap/env/cache.rb +87 -0
  22. data/lib/tap/env/constant.rb +94 -39
  23. data/lib/tap/env/path.rb +71 -0
  24. data/lib/tap/join.rb +42 -78
  25. data/lib/tap/joins/gate.rb +85 -0
  26. data/lib/tap/joins/switch.rb +4 -2
  27. data/lib/tap/joins/sync.rb +3 -3
  28. data/lib/tap/middleware.rb +5 -5
  29. data/lib/tap/middlewares/debugger.rb +18 -58
  30. data/lib/tap/parser.rb +115 -183
  31. data/lib/tap/root.rb +162 -239
  32. data/lib/tap/signal.rb +72 -0
  33. data/lib/tap/signals.rb +20 -2
  34. data/lib/tap/signals/class_methods.rb +38 -43
  35. data/lib/tap/signals/configure.rb +19 -0
  36. data/lib/tap/signals/help.rb +5 -7
  37. data/lib/tap/signals/load.rb +49 -0
  38. data/lib/tap/signals/module_methods.rb +1 -0
  39. data/lib/tap/task.rb +46 -275
  40. data/lib/tap/tasks/dump.rb +21 -16
  41. data/lib/tap/tasks/list.rb +184 -0
  42. data/lib/tap/tasks/load.rb +4 -4
  43. data/lib/tap/tasks/prompt.rb +128 -0
  44. data/lib/tap/tasks/signal.rb +42 -0
  45. data/lib/tap/tasks/singleton.rb +35 -0
  46. data/lib/tap/tasks/stream.rb +64 -0
  47. data/lib/tap/utils.rb +83 -0
  48. data/lib/tap/version.rb +2 -2
  49. data/lib/tap/workflow.rb +124 -0
  50. data/tap.yml +0 -0
  51. metadata +59 -24
  52. data/cmd/console.rb +0 -43
  53. data/cmd/manifest.rb +0 -118
  54. data/cmd/run.rb +0 -145
  55. data/doc/Examples/Workflow +0 -40
  56. data/lib/tap/app/node.rb +0 -29
  57. data/lib/tap/env/context.rb +0 -61
  58. data/lib/tap/env/gems.rb +0 -63
  59. data/lib/tap/env/manifest.rb +0 -179
  60. data/lib/tap/env/minimap.rb +0 -308
  61. data/lib/tap/intern.rb +0 -50
  62. data/lib/tap/joins.rb +0 -9
  63. data/lib/tap/prompt.rb +0 -36
  64. data/lib/tap/root/utils.rb +0 -220
  65. data/lib/tap/root/versions.rb +0 -138
  66. data/lib/tap/signals/signal.rb +0 -68
data/lib/tap.rb CHANGED
@@ -1,6 +1,75 @@
1
- lib = File.expand_path(File.dirname(__FILE__))
2
- $:.unshift(lib) unless $:.include?(lib)
3
-
1
+ require 'tap/declarations'
4
2
  require 'tap/version'
5
- require 'tap/app'
6
- require 'tap/task'
3
+
4
+ module Tap
5
+ module_function
6
+
7
+ def options
8
+ options = {
9
+ :tapfile => ENV['TAPFILE'],
10
+ :gems => ENV['TAP_GEMS'],
11
+ :path => ENV['TAP_PATH'],
12
+ :tapenv => ENV['TAPENV'],
13
+ :taprc => ENV['TAPRC'],
14
+ :tap_cache => ENV['TAP_CACHE'],
15
+ :debug => ENV['TAP_DEBUG']
16
+ }
17
+ options
18
+ end
19
+
20
+ def setup(options=self.options)
21
+ env = Env.new
22
+ app = App.new({}, :env => env)
23
+ app.set('app', app)
24
+ app.set('env', env)
25
+ App.current = app
26
+
27
+ def options.process(key, default=nil)
28
+ value = self[key] || default
29
+ if self[:debug] == 'true'
30
+ $stderr.puts(App::LOG_FORMAT % [' ', nil, key, value])
31
+ end
32
+ value && block_given? ? yield(value) : nil
33
+ end
34
+
35
+ if options[:debug] == 'true'
36
+ options.process(:ruby, "#{RbConfig::CONFIG['RUBY_INSTALL_NAME']}-#{RUBY_VERSION} (#{RUBY_RELEASE_DATE})")
37
+ options.process(:tap, VERSION)
38
+ app.debug = true
39
+ app.verbose = true
40
+ app.logger.level = Logger::DEBUG
41
+ end
42
+
43
+ options.process(:gems) do |gems|
44
+ cache_dir = options[:tap_cache]
45
+
46
+ if cache_dir.to_s.strip.empty?
47
+ require 'tmpdir'
48
+ cache_dir = Dir.tmpdir
49
+ end
50
+
51
+ env.signal(:load).call Env::Cache.new(cache_dir, options[:debug]).select(gems)
52
+ end
53
+
54
+ options.process(:path) do |path|
55
+ Env::Path.split(path).each {|dir| env.auto(:dir => dir) }
56
+ end
57
+
58
+ options.process(:tapenv) do |tapenv_path|
59
+ env.signal(:load).call Env::Path.split(tapenv_path)
60
+ end
61
+
62
+ options.process(:taprc) do |taprc_path|
63
+ app.signal(:load).call Env::Path.split(taprc_path)
64
+ end
65
+
66
+ options.process(:tapfile) do |tapfile_path|
67
+ Env::Path.split(tapfile_path).each do |tapfile|
68
+ next unless File.file?(tapfile)
69
+ Declarations::Context.new(app, File.basename(tapfile)).instance_eval(File.read(tapfile), tapfile, 1)
70
+ end
71
+ end
72
+
73
+ app
74
+ end
75
+ end
@@ -1,46 +1,49 @@
1
1
  require 'logger'
2
- require 'tap/app/api'
3
- require 'tap/app/node'
2
+ require 'tap/env'
4
3
  require 'tap/app/state'
5
4
  require 'tap/app/stack'
6
5
  require 'tap/app/queue'
7
- require 'tap/env'
8
- require 'tap/parser'
6
+ autoload(:YAML, 'yaml')
9
7
 
10
8
  module Tap
11
-
12
- # :startdoc::app
13
- #
14
9
  # App coordinates the setup and execution of workflows.
15
10
  class App
16
11
  class << self
17
- # Sets the current app instance
18
- attr_writer :instance
12
+ def set_context(context={})
13
+ current = Thread.current[CONTEXT]
14
+ Thread.current[CONTEXT] = context
15
+ current
16
+ end
17
+
18
+ def with_context(context)
19
+ begin
20
+ current = set_context(context)
21
+ yield
22
+ ensure
23
+ set_context(current)
24
+ end
25
+ end
19
26
 
20
- # Returns the current instance of App. If no instance has been set,
21
- # then instance initializes a new App with the default configuration.
22
- # Instance is used to initialize tasks when no app is specified and
23
- # exists for convenience only.
24
- def instance(auto_initialize=true)
25
- @instance ||= (auto_initialize ? new : nil)
27
+ def context
28
+ Thread.current[CONTEXT] ||= {}
26
29
  end
27
30
 
28
- # Sets up and returns App.instance with an Env setup to the specified
29
- # directory. This method is used to initialize the app and env as seen
30
- # by the tap executable.
31
- def setup(dir=Dir.pwd)
32
- env = Env.setup(dir)
33
- @instance = new(:env => env)
31
+ def current=(app)
32
+ context[CURRENT] = app
34
33
  end
35
34
 
36
- def build(spec={}, app=nil)
37
- config = spec['config'] || {}
35
+ def current
36
+ context[CURRENT] ||= new
37
+ end
38
+
39
+ def build(spec={}, app=current)
40
+ config = spec['config'] || {}
38
41
  signals = spec['signals'] || []
39
42
 
40
43
  if spec['self']
41
44
  app.reconfigure(config)
42
45
  else
43
- app = new(config)
46
+ app = new(config, :env => app.env)
44
47
  end
45
48
 
46
49
  signals.each do |args|
@@ -55,7 +58,12 @@ module Tap
55
58
  include Configurable
56
59
  include MonitorMixin
57
60
  include Signals
58
- include Node
61
+
62
+ # A variable to store the application context in Thread.current
63
+ CONTEXT = 'tap.context'
64
+
65
+ # A variable to store an instance in the application context.
66
+ CURRENT = 'tap.current'
59
67
 
60
68
  # The reserved call keys
61
69
  CALL_KEYS = %w{obj sig args}
@@ -66,17 +74,30 @@ module Tap
66
74
  # Reserved call and init keys as a single array
67
75
  RESERVED_KEYS = CALL_KEYS + INIT_KEYS
68
76
 
69
- # The default App logger (writes to $stderr at level INFO)
70
- DEFAULT_LOGGER = Logger.new($stderr)
71
- DEFAULT_LOGGER.level = Logger::INFO
72
- DEFAULT_LOGGER.formatter = lambda do |severity, time, progname, msg|
73
- " %s[%s] %18s %s\n" % [severity[0,1], time.strftime('%H:%M:%S') , progname || '--' , msg]
77
+ # Splits a signal into an object string and a signal string. If OBJECT
78
+ # doesn't match, then the string can be considered a signal, and the
79
+ # object is nil. After a match:
80
+ #
81
+ # $1:: The object string
82
+ # (ex: 'obj/sig' => 'obj')
83
+ # $2:: The signal string
84
+ # (ex: 'obj/sig' => 'sig')
85
+ #
86
+ OBJECT = /\A(.*)\/(.*)\z/
87
+
88
+ # The default log format
89
+ LOG_FORMAT = "%s %8s %10s %s\n"
90
+
91
+ # The default logger formatter -- uses LOG_FORMAT
92
+ LOG_FORMATTER = lambda do |severity, time, head, tail|
93
+ code = (severity == 'INFO' ? ' ' : severity[0,1])
94
+ LOG_FORMAT % [code, time.strftime('%H:%M:%S'), head, tail]
74
95
  end
75
96
 
76
97
  # The state of the application (see App::State)
77
98
  attr_reader :state
78
99
 
79
- # The application call stack for executing nodes
100
+ # The application call stack for executing tasks
80
101
  attr_reader :stack
81
102
 
82
103
  # The application queue
@@ -88,60 +109,96 @@ module Tap
88
109
  # The application logger
89
110
  attr_accessor :logger
90
111
 
91
- config :debug, false, :short => :d, &c.flag # Flag debugging
92
- config :force, false, :short => :f, &c.flag # Force execution at checkpoints
93
- config :quiet, false, :short => :q, &c.flag # Suppress logging
94
- config :verbose, false, :short => :v, &c.flag # Enables extra logging (overrides quiet)
95
- config :auto_enque, true, &c.switch # Auto-enque parsed args
96
- config :bang, true, &c.switch # Use parse! when possible
112
+ # The application environment
113
+ attr_accessor :env
97
114
 
98
- nest :env, Env, # The application environment
99
- :type => :hidden,
100
- :writer => false,
101
- :init => false
115
+ config :debug, false, :short => :d, &c.flag # Flag debugging
116
+ config :force, false, :short => :f, &c.flag # Force execution at checkpoints
117
+ config :quiet, false, :short => :q, &c.flag # Suppress logging
118
+ config :verbose, false, :short => :v, &c.flag # Enables extra logging (overrides quiet)
119
+
120
+ define_signal :exe do |input| # executes an object
121
+ spec = convert_to_hash(input, ['var'], 'input')
122
+ obj.exe obj.obj(spec['var']), spec['input']
123
+ end
124
+
125
+ define_signal :enq do |input| # enques an object
126
+ spec = convert_to_hash(input, ['var'], 'input')
127
+ obj.enq obj.obj(spec['var']), spec['input']
128
+ end
102
129
 
103
- signal_hash :set, # set or unset objects
130
+ define_signal :pq do |input| # priority-enques an object
131
+ spec = convert_to_hash(input, ['var'], 'input')
132
+ obj.pq obj.obj(spec['var']), spec['input']
133
+ end
134
+
135
+ signal_hash :set, # set or unset objects
104
136
  :signature => ['var', 'class'],
105
137
  :remainder => 'spec',
106
138
  :bind => :build
107
-
108
- signal :get, :signature => ['var'] # get objects
109
139
 
110
- signal_class :list do # list available objects
111
- def call(args) # :nodoc:
112
- lines = obj.objects.collect {|(key, obj)| "#{key}: #{obj.class}" }
113
- lines.empty? ? "No objects yet..." : lines.sort.join("\n")
114
- end
140
+ signal :get, # get objects
141
+ :signature => ['var']
142
+
143
+ signal_hash :bld, # build an object
144
+ :signature => ['class'],
145
+ :remainder => 'spec',
146
+ :bind => :build
147
+
148
+ define_signal :use do |input| # enables middleware
149
+ spec = convert_to_hash(input, ['class'], 'spec')
150
+ obj.stack = obj.build(spec, &block)
115
151
  end
116
152
 
117
- signal :enque # enques an object
153
+ define_signal :configure, Configure # configures the app
118
154
 
119
- signal_class :parse do # parse a workflow
120
- def call(args) # :nodoc:
121
- argv = convert_to_array(args, ['args'])
122
- obj.send(obj.bang ? :parse! : :parse, argv, &block)
123
- end
155
+ signal :reset # reset the app
156
+ signal :run # run the app
157
+ signal :stop # stop the app
158
+ signal :terminate # terminate the app
159
+ signal :info # prints app status
160
+
161
+ define_signal :list do |input| # list available objects
162
+ lines = obj.objects.collect {|(key, obj)| "#{key}: #{obj.class}" }
163
+ lines.empty? ? "No objects yet..." : lines.sort.join("\n")
124
164
  end
125
165
 
126
- signal_class :use do # enables middleware
127
- def call(args) # :nodoc:
128
- spec = convert_to_hash(args, ['class'], 'spec')
129
- obj.stack = obj.build(spec, &block)
166
+ define_signal :serialize do |input| # serialize the app as signals
167
+ if input.kind_of?(Array)
168
+ psr = ConfigParser.new
169
+ psr.on('--[no-]bare') {|value| psr['bare'] = value }
170
+ path, *ignored = psr.parse!(input)
171
+ psr['path'] = path
172
+ input = psr.config
130
173
  end
174
+
175
+ bare = input.has_key?('bare') ? input['bare'] : true
176
+ signals = obj.serialize(bare)
177
+
178
+ if path = input['path']
179
+ File.open(path, "w") {|io| YAML.dump(signals, io) }
180
+ else
181
+ YAML.dump(signals, $stdout)
182
+ end
183
+
184
+ obj
131
185
  end
132
186
 
133
- signal :run # run the app
134
- signal :stop # stop the app
135
- signal :terminate # terminate the app
136
- signal :info # prints app status
137
-
138
- signal_class :exit do # exit immediately
139
- def process(args) # :nodoc:
140
- exit(1)
187
+ define_signal :import do |input| # import serialized signals
188
+ paths = convert_to_array(input, ['paths'])
189
+ paths.each do |path|
190
+ YAML.load_file(path).each do |signal|
191
+ obj.call(signal)
192
+ end
141
193
  end
194
+
195
+ obj
142
196
  end
143
197
 
144
- signal :help, :class => Help, :bind => nil # signals help
198
+ define_signal :load, Load # load a tapfile
199
+ define_signal :help, Help # signals help
200
+
201
+ cache_signals
145
202
 
146
203
  # Creates a new App with the given configuration. Options can be used to
147
204
  # specify objects that are normally initialized for every new app:
@@ -150,36 +207,27 @@ module Tap
150
207
  # :queue the application queue; an App::Queue
151
208
  # :objects application objects; a hash of (var, object) pairs
152
209
  # :logger the application logger
210
+ # :env the application environment
153
211
  #
154
212
  # A block may also be provided; it will be set as a default join.
155
- def initialize(config={}, options={}, &block)
213
+ def initialize(config={}, options={})
156
214
  super() # monitor
157
215
 
158
216
  @state = State::READY
159
217
  @stack = options[:stack] || Stack.new(self)
160
218
  @queue = options[:queue] || Queue.new
161
219
  @objects = options[:objects] || {}
162
- @logger = options[:logger] || DEFAULT_LOGGER
163
- @joins = []
164
- on_complete(&block)
220
+ @logger = options[:logger] || begin
221
+ logger = Logger.new($stderr)
222
+ logger.level = Logger::INFO
223
+ logger.formatter = LOG_FORMATTER
224
+ logger
225
+ end
226
+ @env = options[:env] || Env.new
165
227
 
166
- self.env = config.delete(:env) || config.delete('env')
167
228
  initialize_config(config)
168
229
  end
169
230
 
170
- # Sets the application environment and validates that env provides an AGET
171
- # ([]) and invert method. AGET is used to lookup constants during init;
172
- # it receives the 'class' parameter and should return a corresponding
173
- # class. Invert should return an object that reverses the AGET lookup.
174
- # Tap::Env and a regular Hash both satisfy this api.
175
- #
176
- # Env can be set to nil and is set to nil by default, but initialization
177
- # is constrained without it.
178
- def env=(env)
179
- Validation.validate_api(env, [:[], :invert]) unless env.nil?
180
- @env = env
181
- end
182
-
183
231
  # Sets the application stack.
184
232
  def stack=(stack)
185
233
  synchronize do
@@ -187,11 +235,6 @@ module Tap
187
235
  end
188
236
  end
189
237
 
190
- # True if the debug config or the global variable $DEBUG is true.
191
- def debug?
192
- debug || $DEBUG
193
- end
194
-
195
238
  # Logs the action and message at the input level (default INFO). The
196
239
  # message may be generated by a block; in that case leave the message
197
240
  # unspecified as nil.
@@ -214,37 +257,23 @@ module Tap
214
257
  # log(:action, "but a message with #{a}, #{b}, #{c}, and #{d}")
215
258
  # log(:action) { "may be #{best} in a block because you can #{turn} #{it} #{off}" }
216
259
  #
217
- def log(action, msg=nil, level=Logger::INFO)
260
+ def log(action='', msg=nil, level=Logger::INFO)
218
261
  if !quiet || verbose
219
- msg ||= yield
220
- logger.add(level, msg, action.to_s)
262
+ msg = yield if msg.nil? && block_given?
263
+ logger.add(level, msg.to_s, action.to_s)
221
264
  end
222
265
  end
223
266
 
224
- # Returns a new node that executes block on call.
225
- def node(&block) # :yields: *inputs
226
- Node.intern(&block)
267
+ # Enques the task with the input. Returns the task.
268
+ def enq(task, input=[])
269
+ queue.enq(task, input)
270
+ task
227
271
  end
228
272
 
229
- # Enques the node with the inputs. Returns the node.
230
- def enq(node, *inputs)
231
- queue.enq(node, inputs)
232
- node
233
- end
234
-
235
- # Generates a node from the block and enques. Returns the new node.
236
- def bq(*inputs, &block) # :yields: *inputs
237
- node = self.node(&block)
238
- queue.enq(node, inputs)
239
- node
240
- end
241
-
242
- # Adds the specified middleware to the stack. The argv will be used as
243
- # extra arguments to initialize the middleware.
244
- def use(middleware, *argv)
245
- synchronize do
246
- @stack = middleware.new(@stack, *argv)
247
- end
273
+ # Priority-enques (unshifts) the task with the input. Returns the task.
274
+ def pq(task, input=[])
275
+ queue.unshift(task, input)
276
+ task
248
277
  end
249
278
 
250
279
  # Sets the object to the specified variable and returns obj. Provide nil
@@ -321,13 +350,20 @@ module Tap
321
350
  # signal.call(args) # call the signal with args
322
351
  #
323
352
  # Call returns the result of the signal call.
324
- #
325
353
  def call(args, &block)
326
- obj = args['obj']
327
- sig = args['sig']
354
+ log(:call, nil, Logger::DEBUG) { args.inspect } if debug
355
+
356
+ obj = args['obj']
357
+ sig = args['sig']
328
358
  args = args['args'] || args
329
359
 
330
- route(obj, sig, &block).call(args)
360
+ # nil obj routes back to app, so optimize by evaluating signal directly
361
+ (obj.nil? ? signal(sig, &block) : route(obj, sig, &block)).call(args)
362
+ end
363
+
364
+ def signal(sig, &block)
365
+ sig = sig.to_s
366
+ sig =~ OBJECT ? route($1, $2, &block) : super(sig, &block)
331
367
  end
332
368
 
333
369
  def route(obj, sig, &block)
@@ -341,41 +377,22 @@ module Tap
341
377
 
342
378
  object.signal(sig, &block)
343
379
  end
344
-
345
- def resolve(const_str)
346
- constant = env ? env[const_str] : Env::Constant.constantize(const_str)
347
- constant or raise "unresolvable constant: #{const_str.inspect}"
380
+
381
+ # Resolves the class in env and initializes a new instance with the args
382
+ # and block. Note that the app is not appended to args by default.
383
+ def init(clas, *args, &block)
384
+ env.constant(clas).new(*args, &block)
348
385
  end
349
386
 
350
- def build(spec)
351
- var = spec['var']
387
+ def build(spec, &block)
388
+ var = spec['var']
352
389
  clas = spec['class']
353
390
  spec = spec['spec'] || spec
354
- obj = nil
355
-
356
- if clas.nil?
357
- unless spec.empty?
358
- raise "no class specified"
359
- end
360
- else
361
- clas = resolve(clas)
362
-
363
- case spec
364
- when Array
365
- parse = bang ? :parse! : :parse
366
- obj, args = clas.send(parse, spec, self)
367
391
 
368
- if block_given?
369
- yield(obj, args)
370
- else
371
- warn_ignored_args(args)
372
- end
373
-
374
- when Hash
375
- obj = clas.build(spec, self)
376
- else
377
- raise "invalid spec: #{spec.inspect}"
378
- end
392
+ obj = nil
393
+ unless clas.nil?
394
+ method_name = spec.kind_of?(Array) ? :parse : :build
395
+ obj = env.constant(clas).send(method_name, spec, self, &block)
379
396
  end
380
397
 
381
398
  unless var.nil?
@@ -389,76 +406,12 @@ module Tap
389
406
  obj
390
407
  end
391
408
 
392
- def parse(argv, &block) # :yields: spec
393
- parse!(argv.dup, &block)
394
- end
395
-
396
- def parse!(argv, &block) # :yields: spec
397
- parser = Parser.new
398
- argv = parser.parse!(argv)
399
-
400
- # The queue API does not provide a delete method, so picking out the
401
- # deque jobs requires the whole queue be cleared, then re-enqued.
402
- # Safety (and speed) is improved with synchronization.
403
- queue.synchronize do
404
- deque = []
405
- blocks = {}
406
-
407
- if auto_enque
408
- blocks[:node] = lambda do |obj, args|
409
- queue.enq(obj, args)
410
- args = nil
411
- end
412
-
413
- blocks[:join] = lambda do |obj, args|
414
- unless obj.respond_to?(:outputs)
415
- # warning
416
- end
417
- deque.concat obj.outputs
418
- end
419
- end
420
-
421
- parser.specs.each do |spec|
422
- if block_given?
423
- next unless yield(spec)
424
- end
425
-
426
- type, obj, sig, *args = spec
427
-
428
- sig_block = case sig
429
- when 'set'
430
- blocks[type]
431
- when 'parse'
432
- block
433
- else
434
- nil
435
- end
436
-
437
- call('obj' => obj, 'sig' => sig, 'args' => args, &sig_block)
438
- end
439
-
440
- deque.uniq!
441
- queue.clear.each do |(obj, args)|
442
- if deque.delete(obj)
443
- warn_ignored_args(args)
444
- else
445
- queue.enq(obj, args)
446
- end
447
- end
448
- end
449
-
450
- argv
451
- end
452
-
453
- # Enques the application object specified by var with args. Raises
454
- # an error if no such application object exists.
455
- def enque(var, *args)
456
- unless node = get(var)
457
- raise "unknown object: #{var.inspect}"
409
+ # Adds the specified middleware to the stack. The argv will be used as
410
+ # extra arguments to initialize the middleware.
411
+ def use(clas, *argv)
412
+ synchronize do
413
+ @stack = init(clas, @stack, *argv)
458
414
  end
459
-
460
- queue.enq(node, args)
461
- node
462
415
  end
463
416
 
464
417
  # Returns an array of middlware in use by self.
@@ -478,7 +431,7 @@ module Tap
478
431
  visited << current
479
432
 
480
433
  if circular_stack
481
- visited.collect! {|middleware| middleware.class.to_s }.join(', ')
434
+ visited.collect! {|m| m.class.to_s }.join(', ')
482
435
  raise "circular stack detected:\n[#{visited}]"
483
436
  end
484
437
  end
@@ -503,53 +456,41 @@ module Tap
503
456
  objects.clear
504
457
  queue.clear
505
458
  end
459
+ self
506
460
  end
507
461
 
508
- # Execute is a wrapper for dispatch allowing inputs to be listed out
509
- # rather than provided as an array.
510
- def execute(node, *inputs)
511
- dispatch(node, inputs)
512
- end
513
-
514
- # Dispatch does the following in order:
462
+ # Executes tasks by doing the following.
515
463
  #
516
- # - call stack with the node and inputs
517
- # - call the node joins (node.joins)
464
+ # - call stack with the task and input
465
+ # - call the task joins (task.joins)
518
466
  #
519
- # The joins for self will be called if the node joins are an empty array.
520
- # No joins will be called if the node joins are nil, or if the node does
521
- # not provide a joins method.
522
- #
523
- # Dispatch returns the stack result.
524
- def dispatch(node, inputs=[])
525
- result = stack.call(node, inputs)
467
+ # Returns the stack result.
468
+ def exe(task, input=[])
469
+ log("#{var(task)} <<", "#{input.inspect} (#{task.class})", Logger::DEBUG) if debug
470
+ output = stack.call(task, input)
471
+ log("#{var(task)} >>", "#{output.inspect} (#{task.class})", Logger::DEBUG) if debug
526
472
 
527
- if node.respond_to?(:joins)
528
- if joins = node.joins
529
-
530
- if joins.empty?
531
- joins = self.joins
532
- end
533
-
473
+ if task.respond_to?(:joins)
474
+ if joins = task.joins
534
475
  joins.each do |join|
535
- join.call(result)
476
+ join.call(output)
536
477
  end
537
478
  end
538
479
  end
539
480
 
540
- result
481
+ output
541
482
  end
542
483
 
543
- # Sequentially dispatches each enqued (node, inputs) pair to the
544
- # application stack. A run continues until the queue is empty.
484
+ # Sequentially executes each enqued job (a [task, input] pair). A run
485
+ # continues until the queue is empty.
545
486
  #
546
- # Run checks the state of self before dispatching a node. If the state
487
+ # Run checks the state of self before executing a task. If the state
547
488
  # changes from RUN, the following behaviors result:
548
- #
549
- # STOP No more nodes will be dispatched; the current node
489
+ #
490
+ # STOP No more tasks will be executed; the current task
550
491
  # will continute to completion.
551
- # TERMINATE No more nodes will be dispatched and the currently
552
- # running node will be discontinued as described in
492
+ # TERMINATE No more tasks will be executed and the currently
493
+ # running task will be discontinued as described in
553
494
  # terminate.
554
495
  #
555
496
  # Calls to run when the state is not READY do nothing and return
@@ -564,12 +505,12 @@ module Tap
564
505
 
565
506
  begin
566
507
  while state == State::RUN
567
- break unless entry = queue.deq
568
- dispatch(*entry)
508
+ break unless job = queue.deq
509
+ exe(*job)
569
510
  end
570
511
  rescue(TerminateError)
571
512
  # gracefully fail for termination errors
572
- queue.unshift(*entry)
513
+ queue.unshift(*job)
573
514
  ensure
574
515
  synchronize { @state = State::READY }
575
516
  end
@@ -577,8 +518,8 @@ module Tap
577
518
  self
578
519
  end
579
520
 
580
- # Signals a running app to stop dispatching nodes to the application stack
581
- # by setting state to STOP. The node currently in the stack will continue
521
+ # Signals a running app to stop executing tasks to the application stack
522
+ # by setting state to STOP. The task currently in the stack will continue
582
523
  # to completion.
583
524
  #
584
525
  # Does nothing unless state is RUN.
@@ -593,7 +534,7 @@ module Tap
593
534
  # them quietly.
594
535
  #
595
536
  # Nodes can set breakpoints that call check_terminate to invoke
596
- # node-specific termination. If a node never calls check_terminate, then
537
+ # task-specific termination. If a task never calls check_terminate, then
597
538
  # it will continue to completion.
598
539
  #
599
540
  # Does nothing if state is READY.
@@ -622,42 +563,10 @@ module Tap
622
563
  "state: #{state} (#{State.state_str(state)}) queue: #{queue.size}"
623
564
  end
624
565
 
625
- # Dumps self to the target as YAML. (note dump is still experimental)
626
- #
627
- # ==== Notes
628
- #
629
- # Platforms that use {Syck}[http://whytheluckystiff.net/syck/] (ex MRI)
630
- # require a fix because Syck misformats certain dumps, such that they
631
- # cannot be reloaded (even by Syck). Specifically:
632
- #
633
- # &id001 !ruby/object:Tap::Task ?
634
- #
635
- # should be:
636
- #
637
- # ? &id001 !ruby/object:Tap::Task
638
- #
639
- # Dump fixes this error and, in addition, removes Thread and Proc dumps
640
- # because they can't be allocated on load.
641
- def dump(target=$stdout, options={})
642
- synchronize do
643
- options = {
644
- :date_format => '%Y-%m-%d %H:%M:%S',
645
- :date => true,
646
- :info => true
647
- }.merge(options)
648
-
649
- # print basic headers
650
- target.puts "# date: #{Time.now.strftime(options[:date_format])}" if options[:date]
651
- target.puts "# info: #{info}" if options[:info]
652
-
653
- # dump yaml, fixing as necessary
654
- yaml = YAML.dump(self)
655
- yaml.gsub!(/\&(.*!ruby\/object:.*?)\s*\?/) {"? &#{$1} " } if YAML.const_defined?(:Syck)
656
- yaml.gsub!(/!ruby\/object:(Thread|Proc) \{\}/, '')
657
- target << yaml
658
- end
659
-
660
- target
566
+ # Sets self as instance in the current context, for the duration of the
567
+ # block (see App.with_context).
568
+ def scope
569
+ App.with_context(CURRENT => self) { yield }
661
570
  end
662
571
 
663
572
  # Converts the self to a schema that can be used to build a new app with
@@ -677,8 +586,8 @@ module Tap
677
586
  order = []
678
587
 
679
588
  # collect enque signals to setup queue
680
- signals = queue.to_a.collect do |(node, args)|
681
- {'sig' => 'enque', 'args' => [var(node)] + args}
589
+ signals = queue.to_a.collect do |(task, input)|
590
+ {'sig' => 'enq', 'args' => {'var' => var(task), 'input' => input}}
682
591
  end
683
592
 
684
593
  # collect and trace application objects
@@ -708,7 +617,6 @@ module Tap
708
617
  (variables[obj] ||= []) << var
709
618
  end
710
619
 
711
- invert_env = env ? env.invert : nil
712
620
  specs.keys.each do |obj|
713
621
  spec = {'sig' => 'set'}
714
622
 
@@ -722,9 +630,7 @@ module Tap
722
630
  end
723
631
 
724
632
  # assign the class
725
- klass = obj.class
726
- klass = invert_env[klass] if invert_env
727
- spec['class'] = klass.to_s
633
+ spec['class'] = obj.class.to_s
728
634
 
729
635
  # merge obj_spec if possible
730
636
  obj_spec = specs[obj]
@@ -770,17 +676,10 @@ module Tap
770
676
  def inspect
771
677
  "#<#{self.class}:#{object_id} #{info}>"
772
678
  end
773
-
679
+
774
680
  private
775
-
776
- # warns of ignored args
777
- def warn_ignored_args(args) # :nodoc:
778
- if args && debug? && !args.empty?
779
- warn "ignoring args: #{args.inspect}"
780
- end
781
- end
782
681
 
783
- # Traces each object backwards and forwards for node, joins, etc. and adds
682
+ # Traces each object backwards and forwards for tasks, joins, etc. and adds
784
683
  # each to specs as needed. The trace determines and returns the order in
785
684
  # which these specs must be initialized to make sense. Circular traces
786
685
  # are detected.
@@ -812,6 +711,10 @@ module Tap
812
711
  # obj must exist before brefs (back-references)
813
712
  if obj.respond_to?(:associations)
814
713
  refs, brefs = obj.associations
714
+
715
+ unless array_or_nil?(refs) && array_or_nil?(brefs)
716
+ raise "invalid associations on object (refs, brefs must be an array or nil): #{obj.inspect}"
717
+ end
815
718
 
816
719
  refs.each {|ref| trace(ref, specs, order) } if refs
817
720
  order << obj
@@ -823,12 +726,16 @@ module Tap
823
726
  order
824
727
  end
825
728
 
729
+ def array_or_nil?(obj) # :nodoc:
730
+ obj.nil? || obj.kind_of?(Array)
731
+ end
732
+
826
733
  def self_to_spec # :nodoc:
827
734
  config = self.config.to_hash {|hash, key, value| hash[key.to_s] = value }
828
735
  {'config' => config, 'self' => true}
829
736
  end
830
737
 
831
- # TerminateErrors are raised to kill executing nodes when terminate is
738
+ # TerminateErrors are raised to kill executing tasks when terminate is
832
739
  # called on an running App. They are handled by the run rescue code.
833
740
  class TerminateError < RuntimeError
834
741
  end