tap 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/doc/API CHANGED
@@ -1,83 +1,223 @@
1
1
  = Application Programming Interface
2
2
 
3
- (Note these are all currently provisional.)
4
-
5
- == Tap::App
6
-
7
- Applications require the following API for nodes, joins, and middleware. Tap
8
- provides modules or base classes that implement these APIs and may be used as
9
- the foundation for subclasses.
10
-
11
- ==== Node
12
-
13
- call(*inputs) any return is allowed
14
- joins() returns an array of joins
15
-
16
- The signature for call can be modified as necessary.
3
+ Applications require the following methods for nodes, joins, and middleware.
4
+ Tap provides modules and base classes that implement these APIs.
5
+
6
+ === {Node}[link:classes/Tap/App/Node.html] ({Task}[link:classes/Tap/Task.html])
7
+
8
+ call(*inputs) # any return is allowed
9
+ joins() # returns an array of joins, or nil (optional)
10
+
11
+ The signature for call defines the arguments that must be enqued to the node
12
+ or passed to the node via a join. All signature constructs are allowed
13
+ including multiple arguments, default arguments and splats (blocks are
14
+ technically allowed but will never receive a value).
15
+
16
+ The optional joins method specifies an array of joins to be called by a
17
+ running application when the node completes. Each join is called in order. An
18
+ empty array specifies the default application joins should be called; nil
19
+ specifies that no joins should be called. No joins will be called when joins
20
+ is left undefined.
21
+
22
+ === {Join}[link:classes/Tap/Join.html]
23
+
24
+ call(result) # any return is allowed
25
+
26
+ The call method receives the result of input nodes. The result of call is not
27
+ used; call must internally performing the join actions.
28
+
29
+ === {Middleware}[link:classes/Tap/Middleware.html]
30
+
31
+ Middleware.new(stack, *args) # returns an instance of middleware
32
+ call(node, inputs=[]) # any return is allowed
33
+ stack() # returns the original stack
34
+
35
+ Middleware wraps the execution of nodes. Nodes and inputs are passed to the
36
+ middleware during execution; the middleware is responsible for processing the
37
+ node or passing it into the stack using the same call API. By default the base
38
+ stack invokes call on the node with the inputs. Joins are performed after the
39
+ middleware returns.
40
+
41
+ == Application Interface
42
+
43
+ In addition to the APIs for individual workflow objects, Tap defines an
44
+ application interface allowing objects to be created and modified using
45
+ signals sent to an application (think HTTP to a web app). Signals are simple
46
+ hash constructs and typically form the basis for user interfaces. The
47
+ application interface is distinct from the object APIs, although it is typical
48
+ to implement the application interface on top of an object API.
49
+
50
+ Tap::App::Api implements the application interface in a general way and is the
51
+ baseclass for Tap::Task, Tap::Join, and Tap::Middleware. The basic idea is to
52
+ allow instantiation of a class from a hash and serialization of an instance
53
+ back into a hash. The hash is referred to as a specification and must be
54
+ serializable as {JSON}[http://json.org/], basically meaning the hash must
55
+ consist of simple object types: numbers, strings, hashes, and arrays.
56
+
57
+ The application interface consists of two methods, build and to_spec:
58
+
59
+ class Stub
60
+ class << self
61
+ # Build takes a specification hash and returns an instance of self.
62
+ # The spec must be serializable as JSON.
63
+ def build(spec={}, app=Tap::App.instance)
64
+ end
65
+ end
66
+
67
+ # Takes no inputs and returns a specification hash that, when built,
68
+ # returns an object like self.
69
+ #
70
+ # obj.class.build(obj.to_spec) # => returns an object like obj
71
+ #
72
+ # Users can determine for themselves what constitutes 'likeness'.
73
+ def to_spec
74
+ end
75
+ end
17
76
 
18
- ==== Join
77
+ The application API reserves several additional methods that do not need to be
78
+ implemented but add functionality for specific, common use cases. If they are
79
+ present they must adhere to these specifications.
80
+
81
+ # Optional methods #
82
+ class Stub
83
+ class << self
84
+ # Parse takes an argument vector (an array, usually from the command
85
+ # line) and returns an instance of self and any remaining arguments
86
+ # in an array like [instance, args]. The remaining arguments may be
87
+ # nil. Parse cannot modify argv.
88
+ def parse(argv=ARGV, app=Tap::App.instance)
89
+ end
90
+
91
+ # Same as parse, but able to modify argv.
92
+ def parse!(argv=ARGV, app=Tap::App.instance)
93
+ end
94
+ end
95
+
96
+ # Takes a signal name and returns an object that responds to call; the
97
+ # call method invokes the signal actions.
98
+ def signal(sig)
99
+ end
100
+
101
+ # Returns a nested array of workflow objects associated with self (ex
102
+ # input/output nodes for a join). The array should be structured like
103
+ # [refs, brefs], where refs are references to objects that must be built
104
+ # BEFORE self and brefs are back-references to objects that must be built
105
+ # AFTER self.
106
+ #
107
+ # For example, nodes must be built before joins. As such, the associations
108
+ # method for a node returns a brefs for each of its joins. Similarly, joins
109
+ # must be built after nodes and hence the associations method for a join
110
+ # returns refs to their input and output nodes:
111
+ #
112
+ # node.associations # => [nil, join]
113
+ # join.associations # => [inputs + outputs, nil]
114
+ #
115
+ # Nil is a valid return for associations, indicating no associations.
116
+ def associations
117
+ end
118
+ end
19
119
 
20
- call(result) any return is allowed
120
+ The parse methods are used for building objects from interfaces that provide
121
+ an array of inputs (ex the command line) rather a hash; without them objects
122
+ are effectively excluded from use within these interfaces.
21
123
 
22
- ==== Middleware
124
+ Signals can be used to interact with specific objects from a user interface
125
+ much as signals can interact with an app. Objects without a signal method
126
+ cannot receive signals.
23
127
 
24
- Middleware.new(stack) returns an instance of middleware
25
- call(node, inputs=[]) any return is allowed
26
- stack() returns the original stack
128
+ The associations method is used to order complex builds and is described in
129
+ more detail below.
27
130
 
28
- The middleware API is essentially the same as for {Rack}[http://rack.rubyforge.org/].
131
+ === Spec References
29
132
 
30
- == Tap::Schema
133
+ Specifications often require references to other resources, as when a join
134
+ refers to input and output nodes. These references are normally specified as
135
+ variables that, unlike the resource itself, are easily serializable as JSON
136
+ and may be used in multiple places. Apps are constructed to do this easily via
137
+ the +obj+ and +var+ methods.
31
138
 
32
- Schema describe workflows as data. To build a workflow from a schema, workflow
33
- resources like nodes, joins, and middleware need to instantiate themselves
34
- using the schema data. The <tt>parse!</tt> and <tt>instantiate</tt> methods
35
- must be provided to do so.
139
+ As an example, consider the Sample class that references some other
140
+ application object:
36
141
 
37
- Resource.parse!(argv=ARGV, app=App.instance)
38
- Resource.instantiate(argh, app=App.instance)
142
+ class Sample
143
+ def initialize(object)
144
+ @object = object
145
+ end
146
+
147
+ def to_spec
148
+ {'key' => app.var(@object)} # store a variable into the spec
149
+ end
150
+
151
+ def associations
152
+ [[@object], nil] # establish a build order
153
+ end
154
+
155
+ class << self
156
+ def build(spec={}, app=Tap::App.instance)
157
+ object = app.obj(spec['key']) # retrieve an object referenced by the spec
158
+ new(object)
159
+ end
160
+ end
161
+ end
39
162
 
40
- As implied in by the inputs, <tt>parse!</tt> instantiates from an array, while
41
- <tt>instantiate</tt> instantiates from a hash with symbol keys. If
42
- <tt>parse!</tt> receives a string, it must be able to convert it to an array
43
- (ex using Shellwords).
163
+ Using this technique the spec will have a serializable variable representing
164
+ the object and the app will be able to properly schematize and rebuild the
165
+ instance and all its references. Apps use the associations array to determine
166
+ the correct build order for the references. In the example the @object
167
+ reference must be built before the Sample instance and correspondingly the
168
+ associations method returns @object in the 'ref' array.
44
169
 
45
- How the class actually performs the instantiation is up to the class but
46
- typically parse creates a hash and calls instantiate.
170
+ Note that only references to objects implementing the application interface
171
+ may be stored this way; references to objects that do not implement the
172
+ application interface must be serialized and deserialized by the build/to_spec
173
+ methods internally.
47
174
 
48
- == Tap::Env
175
+ === Resource Identifiers
49
176
 
50
- Envs identify resources by resource identifiers (ie constant attributes
51
- recognized by Lazydoc). This identifies 'Sample' as an 'example' resource.
177
+ Tap discovers application resources using resource identifiers (ie constant
178
+ attributes, see {Lazydoc}[http://tap.rubyforge.org/lazydoc]). Resources
179
+ identified in this way can be automatically loaded by the Tap::Env. If no
180
+ identifiers are specified for a resource, the user must manually load the
181
+ resource files.
52
182
 
53
- [file.rb]
183
+ As an example, this identifies the Sample constant as an 'example' resource.
54
184
 
185
+ [lib/file.rb]
186
+
55
187
  # Sample::example summary
56
- # description
57
188
  class Sample
58
189
  end
59
190
 
60
- The constant name will be inferred from the path for the file containing the
61
- resource identifier if no constant name is specified.
191
+ A resource can be identified by zero or more identifiers. Typically all
192
+ identifiers will be put in the same file as the class, but this does not have
193
+ to be the case; applications automatically require all files that identify a
194
+ resource. The order in which the files are required is indeterminate and it is
195
+ up to the user to ensure consistency. For example:
62
196
 
63
- [sample.rb]
197
+ [a.rb]
198
+ # Sample::a
199
+ class Sample
200
+ end
64
201
 
65
- # ::example summary
66
- # description
202
+ [b.rb]
203
+ require 'a'
204
+
205
+ # Sample::b
67
206
  class Sample
68
207
  end
69
208
 
70
- Resources may be accessed using Tap::Env#manifest. Resources intended to be
71
- discovered by Env must map the identifier documentation to the class 'desc'
72
- method. The easiest way to do so is with a
73
- {lazy_attr}[link:lazydoc/classes/Lazydoc/Attributes.html].
209
+ Here the require statement ensures a.rb is always required before b.rb. Note
210
+ that consistency is automatic when all identifiers are in the same file (and
211
+ thus only one file is required).
212
+
213
+ The constant name will be inferred from file path if no constant name is
214
+ specified. This is the most compact form for identifying a resource:
74
215
 
216
+ [lib/sample.rb]
217
+
75
218
  # ::example summary
76
- # description
77
219
  class Sample
78
- lazy_attr :desc, 'example'
79
220
  end
80
-
81
- Sample::desc.summary # => "summary"
82
- Sample::desc.to_s # => "description"
83
-
221
+
222
+ In this case no constant name is specified, so 'Sample' is inferred from
223
+ 'sample.rb'. Constant names are determined from the path using camelization.
@@ -33,4 +33,30 @@
33
33
  % TAP_GEMS= tap run -T
34
34
  % TAP_GEMS=:all tap run -T
35
35
  % TAP_GEMS=:latest tap run -T
36
- % TAP_GEMS="[rap, tap-tasks]" tap run -T
36
+ % TAP_GEMS="[rap, tap-tasks]" tap run -T
37
+
38
+ == Signals
39
+
40
+ The run command will bring up a signal prompt with the --prompt flag, or by
41
+ sending and interrupt signal. To illustrate the latter, setup an infinite
42
+ loop and hit ctl-c to enter the prompt. To exit, signal stop as shown here:
43
+
44
+ % tap run -- dump --[app][0]q --/0/enq 'hello world'
45
+ hello world
46
+ hello world
47
+ hello world
48
+ ... (this will continue to print until you press ctl-c) ...
49
+
50
+ starting prompt (enter for help):
51
+ --/
52
+ =>
53
+ run # run the app
54
+ stop # stop the app
55
+ terminate # terminate the app
56
+ info # prints app status
57
+ enque
58
+ build
59
+ use
60
+ --//info
61
+ => state: 1 (RUN) queue: 0
62
+ --//stop
data/lib/tap.rb CHANGED
@@ -2,4 +2,5 @@ lib = File.expand_path(File.dirname(__FILE__))
2
2
  $:.unshift(lib) unless $:.include?(lib)
3
3
 
4
4
  require 'tap/version'
5
- require 'tap/exe'
5
+ require 'tap/app'
6
+ require 'tap/task'
@@ -1,98 +1,17 @@
1
1
  require 'logger'
2
- require 'configurable'
2
+ require 'tap/app/api'
3
3
  require 'tap/app/node'
4
4
  require 'tap/app/state'
5
5
  require 'tap/app/stack'
6
6
  require 'tap/app/queue'
7
+ require 'tap/env'
8
+ require 'tap/parser'
7
9
 
8
10
  module Tap
9
11
 
10
- # App coordinates the setup and execution of workflows.
11
- #
12
- # === Workflows
13
- #
14
- # Workflows are composed of nodes and joins such as instances of Tap::Task
15
- # and Tap::Join. The actual workflow exists between nodes; each node can
16
- # specify a join to receive it's output and enque or execute other nodes.
17
- # When a node does not have a join, apps allow the specification of a
18
- # default join to, for instance, aggregate results.
19
- #
20
- # Any object satisfying the correct API[link:files/doc/API.html] can be used
21
- # as a node or join. Apps have helpers to make nodes out of blocks.
22
- #
23
- # app = Tap::App.new
24
- # n = app.node {|*inputs| inputs }
25
- # app.enq(n, 'a', 'b', 'c')
26
- # app.enq(n, 1)
27
- # app.enq(n, 2)
28
- # app.enq(n, 3)
29
- #
30
- # results = []
31
- # app.on_complete {|result| results << result }
32
- #
33
- # app.run
34
- # results # => [['a', 'b', 'c'], [1], [2], [3]]
35
- #
36
- # To construct a workflow, set joins for individual nodes. Here is a simple
37
- # sequence:
38
- #
39
- # n0 = app.node { "a" }
40
- # n1 = app.node {|input| "#{input}.b" }
41
- # n2 = app.node {|input| "#{input}.c"}
42
- #
43
- # n0.on_complete {|result| app.execute(n1, result) }
44
- # n1.on_complete {|result| app.execute(n2, result) }
45
- # app.enq(n0)
46
- #
47
- # results.clear
48
- # app.run
49
- # results # => ["a.b.c"]
50
- #
51
- # Tasks have helpers to simplify the manual constructon of workflows, but
52
- # even with these methods large workflows are cumbersome to build. More
53
- # typically, a Tap::Schema is used in such cases.
54
- #
55
- # === Middleware
56
- #
57
- # Apps allow middleware to wrap the execution of each node. This can be
58
- # particularly useful to track the progress of a workflow. Middleware is
59
- # initialized with the application stack and uses the call method to
60
- # wrap the execution of the stack.
61
- #
62
- # Using middleware, an auditor looks like this:
63
- #
64
- # class AuditMiddleware
65
- # attr_reader :stack, :audit
66
- #
67
- # def initialize(stack)
68
- # @stack = stack
69
- # @audit = []
70
- # end
71
- #
72
- # def call(node, inputs=[])
73
- # audit << node
74
- # stack.call(node, inputs)
75
- # end
76
- # end
77
- #
78
- # auditor = app.use AuditMiddleware
79
- #
80
- # app.enq(n0)
81
- # app.enq(n2, "x")
82
- # app.enq(n1, "y")
83
- #
84
- # results.clear
85
- # app.run
86
- # results # => ["a.b.c", "x.c", "y.b.c"]
87
- # auditor.audit
88
- # # => [
89
- # # n0, n1, n2,
90
- # # n2,
91
- # # n1, n2
92
- # # ]
93
- #
94
- # Middleware can be nested with multiple calls to use.
12
+ # :startdoc::app
95
13
  #
14
+ # App coordinates the setup and execution of workflows.
96
15
  class App
97
16
  class << self
98
17
  # Sets the current app instance
@@ -100,18 +19,54 @@ module Tap
100
19
 
101
20
  # Returns the current instance of App. If no instance has been set,
102
21
  # then instance initializes a new App with the default configuration.
103
- #
104
- # Instance is used to initialize tasks when no app is specified. Aside
105
- # from that, there is nothing magical about instance.
22
+ # Instance is used to initialize tasks when no app is specified and
23
+ # exists for convenience only.
106
24
  def instance(auto_initialize=true)
107
25
  @instance ||= (auto_initialize ? new : nil)
108
26
  end
27
+
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)
34
+ end
35
+
36
+ def build(spec={}, app=nil)
37
+ config = spec['config'] || {}
38
+ signals = spec['signals'] || []
39
+
40
+ if spec['self']
41
+ app.reconfigure(config)
42
+ else
43
+ app = new(config)
44
+ end
45
+
46
+ signals.each do |args|
47
+ app.call(args)
48
+ end
49
+
50
+ app.gc
51
+ app
52
+ end
109
53
  end
110
54
 
111
55
  include Configurable
112
56
  include MonitorMixin
57
+ include Signals
58
+ include Node
59
+
60
+ # The reserved call keys
61
+ CALL_KEYS = %w{obj sig args}
62
+
63
+ # The reserved init keys
64
+ INIT_KEYS = %w{var class spec}
113
65
 
114
- # The default App logger writes to $stderr at level INFO.
66
+ # Reserved call and init keys as a single array
67
+ RESERVED_KEYS = CALL_KEYS + INIT_KEYS
68
+
69
+ # The default App logger (writes to $stderr at level INFO)
115
70
  DEFAULT_LOGGER = Logger.new($stderr)
116
71
  DEFAULT_LOGGER.level = Logger::INFO
117
72
  DEFAULT_LOGGER.formatter = lambda do |severity, time, progname, msg|
@@ -127,54 +82,143 @@ module Tap
127
82
  # The application queue
128
83
  attr_reader :queue
129
84
 
130
- # A cache of application-specific data.
131
- attr_reader :cache
132
-
133
- # The default joins for nodes that have no joins set
134
- attr_accessor :default_joins
85
+ # A cache of application objects
86
+ attr_reader :objects
135
87
 
136
88
  # The application logger
137
- attr_reader :logger
89
+ attr_accessor :logger
90
+
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
138
97
 
139
- config :debug, false, &c.flag # Flag debugging
140
- config :force, false, &c.flag # Force execution at checkpoints
141
- config :quiet, false, &c.flag # Suppress logging
142
- config :verbose, false, &c.flag # Enables extra logging (overrides quiet)
98
+ nest :env, Env, # The application environment
99
+ :type => :hidden,
100
+ :writer => false,
101
+ :init => false
143
102
 
144
- # Creates a new App with the given configuration.
103
+ signal_hash :set, # set or unset objects
104
+ :signature => ['var', 'class'],
105
+ :remainder => 'spec',
106
+ :bind => :build
107
+
108
+ signal :get, :signature => ['var'] # get objects
109
+
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
115
+ end
116
+
117
+ signal :enque # enques an object
118
+
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
124
+ end
125
+
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)
130
+ end
131
+ end
132
+
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)
141
+ end
142
+ end
143
+
144
+ signal :help, :class => Help, :bind => nil # signals help
145
+
146
+ # Creates a new App with the given configuration. Options can be used to
147
+ # specify objects that are normally initialized for every new app:
148
+ #
149
+ # :stack the application stack; an App::Stack
150
+ # :queue the application queue; an App::Queue
151
+ # :objects application objects; a hash of (var, object) pairs
152
+ # :logger the application logger
153
+ #
154
+ # A block may also be provided; it will be set as a default join.
145
155
  def initialize(config={}, options={}, &block)
146
156
  super() # monitor
147
157
 
148
158
  @state = State::READY
149
159
  @stack = options[:stack] || Stack.new(self)
150
160
  @queue = options[:queue] || Queue.new
151
- @cache = options[:cache] || {}
152
- @default_joins = []
161
+ @objects = options[:objects] || {}
162
+ @logger = options[:logger] || DEFAULT_LOGGER
163
+ @joins = []
153
164
  on_complete(&block)
154
165
 
166
+ self.env = config.delete(:env) || config.delete('env')
155
167
  initialize_config(config)
156
- self.logger = options[:logger] || DEFAULT_LOGGER
157
168
  end
158
169
 
159
- # True if debug or the global variable $DEBUG is true.
160
- def debug?
161
- debug || $DEBUG
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
162
181
  end
163
182
 
164
- # Sets the current logger. The logger level is set to Logger::DEBUG if
165
- # debug? is true.
166
- def logger=(logger)
167
- unless logger.nil?
168
- logger.level = Logger::DEBUG if debug?
183
+ # Sets the application stack.
184
+ def stack=(stack)
185
+ synchronize do
186
+ @stack = stack
169
187
  end
170
-
171
- @logger = logger
172
188
  end
173
189
 
174
- # Logs the action and message at the input level (default INFO).
190
+ # True if the debug config or the global variable $DEBUG is true.
191
+ def debug?
192
+ debug || $DEBUG
193
+ end
194
+
195
+ # Logs the action and message at the input level (default INFO). The
196
+ # message may be generated by a block; in that case leave the message
197
+ # unspecified as nil.
198
+ #
175
199
  # Logging is suppressed if quiet is true.
176
- def log(action, msg="", level=Logger::INFO)
177
- logger.add(level, msg, action.to_s) if !quiet || verbose
200
+ #
201
+ # ==== Performance Considerations
202
+ #
203
+ # Using a block to generate a message is quicker if logging is off, but
204
+ # slower when logging is on. However, when messages use a lot of
205
+ # interpolation the log time is dominated by the interpolation; at some
206
+ # point the penalty for using a block is outweighed by the benefit of
207
+ # being able to skip the interpolation.
208
+ #
209
+ # For example:
210
+ #
211
+ # log(:action, "this is fast")
212
+ # log(:action) { "and there's not much benefit to the block" }
213
+ #
214
+ # log(:action, "but a message with #{a}, #{b}, #{c}, and #{d}")
215
+ # log(:action) { "may be #{best} in a block because you can #{turn} #{it} #{off}" }
216
+ #
217
+ def log(action, msg=nil, level=Logger::INFO)
218
+ if !quiet || verbose
219
+ msg ||= yield
220
+ logger.add(level, msg, action.to_s)
221
+ end
178
222
  end
179
223
 
180
224
  # Returns a new node that executes block on call.
@@ -195,81 +239,323 @@ module Tap
195
239
  node
196
240
  end
197
241
 
198
- # Adds the specified middleware to the stack.
242
+ # Adds the specified middleware to the stack. The argv will be used as
243
+ # extra arguments to initialize the middleware.
199
244
  def use(middleware, *argv)
200
245
  synchronize do
201
246
  @stack = middleware.new(@stack, *argv)
202
247
  end
203
248
  end
204
249
 
250
+ # Sets the object to the specified variable and returns obj. Provide nil
251
+ # as obj to un-set a variable (in which case the existing object is
252
+ # returned).
253
+ #
254
+ # Nil is reserved as a variable name and cannot be used by set.
255
+ def set(var, obj)
256
+ raise "no var specified" if var.nil?
257
+
258
+ if obj
259
+ objects[var] = obj
260
+ else
261
+ objects.delete(var)
262
+ end
263
+ end
264
+
265
+ # Returns the object set to var, or self if var is nil.
266
+ def get(var)
267
+ var.nil? ? self : objects[var]
268
+ end
269
+
270
+ # Same as get, but raises an error if no object is set to the variable.
271
+ def obj(var)
272
+ get(var) or raise "no object set to: #{var.inspect}"
273
+ end
274
+
275
+ # Returns the variable for the object. If the object is not assigned to a
276
+ # variable and auto_assign is true, then the object is set to an unused
277
+ # variable and the new variable is returned.
278
+ #
279
+ # The new variable will be an integer and will be removed upon gc.
280
+ def var(obj, auto_assign=true)
281
+ objects.each_pair do |var, object|
282
+ return var if obj == object
283
+ end
284
+
285
+ return nil unless auto_assign
286
+
287
+ var = objects.length
288
+ loop do
289
+ if objects.has_key?(var)
290
+ var += 1
291
+ else
292
+ set(var, obj)
293
+ return var
294
+ end
295
+ end
296
+ end
297
+
298
+ # Removes objects keyed by integers. If all is specified, gc will clear
299
+ # all objects.
300
+ def gc(all=false)
301
+ if all
302
+ objects.clear
303
+ else
304
+ objects.delete_if {|var, obj| var.kind_of?(Integer) }
305
+ end
306
+
307
+ self
308
+ end
309
+
310
+ # Sends a signal to an application object. The input should be a hash
311
+ # defining these fields:
312
+ #
313
+ # obj # a variable identifying an object, or nil for self
314
+ # sig # the signal name
315
+ # args # arguments to the signal (typically a Hash)
316
+ #
317
+ # Call does the following:
318
+ #
319
+ # object = app.get(obj) # lookup an application object by obj
320
+ # signal = object.signal(sig) # lookup a signal by sig
321
+ # signal.call(args) # call the signal with args
322
+ #
323
+ # Call returns the result of the signal call.
324
+ #
325
+ def call(args, &block)
326
+ obj = args['obj']
327
+ sig = args['sig']
328
+ args = args['args'] || args
329
+
330
+ route(obj, sig, &block).call(args)
331
+ end
332
+
333
+ def route(obj, sig, &block)
334
+ unless object = get(obj)
335
+ raise "unknown object: #{obj.inspect}"
336
+ end
337
+
338
+ unless object.respond_to?(:signal)
339
+ raise "cannot signal: #{object.inspect}"
340
+ end
341
+
342
+ object.signal(sig, &block)
343
+ 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}"
348
+ end
349
+
350
+ def build(spec)
351
+ var = spec['var']
352
+ clas = spec['class']
353
+ 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
+
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
379
+ end
380
+
381
+ unless var.nil?
382
+ if var.respond_to?(:each)
383
+ var.each {|v| set(v, obj) }
384
+ else
385
+ set(var, obj)
386
+ end
387
+ end
388
+
389
+ obj
390
+ end
391
+
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}"
458
+ end
459
+
460
+ queue.enq(node, args)
461
+ node
462
+ end
463
+
205
464
  # Returns an array of middlware in use by self.
206
465
  def middleware
207
466
  middleware = []
208
467
 
468
+ # collect middleware by walking up the stack
209
469
  synchronize do
210
470
  current = stack
211
- until current.kind_of?(Stack)
471
+ visited = [current]
472
+
473
+ while current.respond_to?(:stack)
212
474
  middleware << current
213
475
  current = current.stack
476
+
477
+ circular_stack = visited.include?(current)
478
+ visited << current
479
+
480
+ if circular_stack
481
+ visited.collect! {|middleware| middleware.class.to_s }.join(', ')
482
+ raise "circular stack detected:\n[#{visited}]"
483
+ end
214
484
  end
215
485
  end
216
486
 
217
487
  middleware
218
488
  end
219
489
 
220
- # Clears the cache, the queue, and resets the stack so that no middleware
221
- # is used. Reset raises an error unless state == State::READY.
490
+ # Clears objects, the queue, and resets the stack so that no middleware
491
+ # is used. Reset raises an error unless state is READY.
222
492
  def reset
223
493
  synchronize do
224
494
  unless state == State::READY
225
495
  raise "cannot reset unless READY"
226
496
  end
227
497
 
228
- @stack = Stack.new(self)
229
- cache.clear
498
+ # walk up middleware to find the base of the stack
499
+ while @stack.respond_to?(:stack)
500
+ @stack = @stack.stack
501
+ end
502
+
503
+ objects.clear
230
504
  queue.clear
231
505
  end
232
506
  end
233
507
 
234
- # Dispatches node to the application stack with the inputs.
508
+ # Execute is a wrapper for dispatch allowing inputs to be listed out
509
+ # rather than provided as an array.
235
510
  def execute(node, *inputs)
236
511
  dispatch(node, inputs)
237
512
  end
238
513
 
239
- # Dispatch sends the node into the application stack with the inputs.
240
514
  # Dispatch does the following in order:
241
515
  #
242
516
  # - call stack with the node and inputs
243
- # - call the node joins, if set, or the default_joins with the results
517
+ # - call the node joins (node.joins)
518
+ #
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.
244
522
  #
245
- # Dispatch returns the node result.
523
+ # Dispatch returns the stack result.
246
524
  def dispatch(node, inputs=[])
247
525
  result = stack.call(node, inputs)
248
526
 
249
- joins = node.joins.empty? ? default_joins : node.joins
250
- joins.each do |join|
251
- join.call(result)
527
+ if node.respond_to?(:joins)
528
+ if joins = node.joins
529
+
530
+ if joins.empty?
531
+ joins = self.joins
532
+ end
533
+
534
+ joins.each do |join|
535
+ join.call(result)
536
+ end
537
+ end
252
538
  end
539
+
253
540
  result
254
541
  end
255
542
 
256
543
  # Sequentially dispatches each enqued (node, inputs) pair to the
257
- # application stack. A run continues until the queue is empty. Returns
258
- # self.
259
- #
260
- # ==== Run State
544
+ # application stack. A run continues until the queue is empty.
261
545
  #
262
546
  # Run checks the state of self before dispatching a node. If the state
263
- # changes from State::RUN, the following behaviors result:
264
- #
265
- # State::STOP:: No more nodes will be dispatched; the current node will
266
- # continute to completion.
267
- # State::TERMINATE:: No more nodes will be dispatched and the currently
268
- # running node will be discontinued as described in
269
- # terminate.
270
- #
271
- # Calls to run when the state is not State::READY do nothing and
272
- # return immediately.
547
+ # changes from RUN, the following behaviors result:
548
+ #
549
+ # STOP No more nodes will be dispatched; the current node
550
+ # will continute to completion.
551
+ # TERMINATE No more nodes will be dispatched and the currently
552
+ # running node will be discontinued as described in
553
+ # terminate.
554
+ #
555
+ # Calls to run when the state is not READY do nothing and return
556
+ # immediately.
557
+ #
558
+ # Returns self.
273
559
  def run
274
560
  synchronize do
275
561
  return self unless state == State::READY
@@ -292,49 +578,51 @@ module Tap
292
578
  end
293
579
 
294
580
  # Signals a running app to stop dispatching nodes to the application stack
295
- # by setting state = State::STOP. The node currently in the stack will
296
- # will continue to completion.
581
+ # by setting state to STOP. The node currently in the stack will continue
582
+ # to completion.
297
583
  #
298
- # Does nothing unless state is State::RUN.
584
+ # Does nothing unless state is RUN.
299
585
  def stop
300
586
  synchronize { @state = State::STOP if state == State::RUN }
301
587
  self
302
588
  end
303
589
 
304
- # Signals a running application to terminate execution by setting
305
- # state = State::TERMINATE. In this state, calls to check_terminate
306
- # will raise a TerminateError. Run considers TerminateErrors a normal
307
- # exit and rescues them quietly.
590
+ # Signals a running application to terminate execution by setting state to
591
+ # TERMINATE. In this state, calls to check_terminate will raise a
592
+ # TerminateError. Run considers TerminateErrors a normal exit and rescues
593
+ # them quietly.
308
594
  #
309
595
  # Nodes can set breakpoints that call check_terminate to invoke
310
596
  # node-specific termination. If a node never calls check_terminate, then
311
- # it will continue to completion and terminate is functionally the same
312
- # as stop.
597
+ # it will continue to completion.
313
598
  #
314
- # Does nothing if state == State::READY.
599
+ # Does nothing if state is READY.
315
600
  def terminate
316
601
  synchronize { @state = State::TERMINATE unless state == State::READY }
317
602
  self
318
603
  end
319
604
 
320
- # Raises a TerminateError if state == State::TERMINATE. Nodes should call
605
+ # Raises a TerminateError if state is TERMINATE. Nodes should call
321
606
  # check_terminate to provide breakpoints in long-running processes.
607
+ #
608
+ # A block may be provided to check_terminate to execute code before
609
+ # raising the TerminateError.
322
610
  def check_terminate
323
- if state == App::State::TERMINATE
324
- yield if block_given?
325
- raise App::TerminateError.new
611
+ if state == State::TERMINATE
612
+ yield() if block_given?
613
+ raise TerminateError.new
326
614
  end
327
615
  end
328
616
 
329
617
  # Returns an information string for the App.
330
618
  #
331
- # App.instance.info # => 'state: 0 (READY) queue: 0'
619
+ # App.new.info # => 'state: 0 (READY) queue: 0'
332
620
  #
333
621
  def info
334
622
  "state: #{state} (#{State.state_str(state)}) queue: #{queue.size}"
335
623
  end
336
624
 
337
- # Dumps self to the target as YAML.
625
+ # Dumps self to the target as YAML. (note dump is still experimental)
338
626
  #
339
627
  # ==== Notes
340
628
  #
@@ -372,14 +660,173 @@ module Tap
372
660
  target
373
661
  end
374
662
 
375
- # Sets the block to receive the result of nodes with no joins
376
- # (ie the block is set as a default_join).
377
- def on_complete(&block) # :yields: _result
378
- self.default_joins << block if block
379
- self
663
+ # Converts the self to a schema that can be used to build a new app with
664
+ # equivalent application objects, queue, and middleware. Schema are a
665
+ # collection of signal hashes such that this will rebuild the state of a
666
+ # on b:
667
+ #
668
+ # a, b = App.new, App.new
669
+ # a.to_schema.each {|spec| b.call(spec) }
670
+ #
671
+ # Application objects that do not satisfy the application object API are
672
+ # quietly ignored; enable debugging to be warned of their existance.
673
+ #
674
+ def serialize(bare=true)
675
+ # setup variables
676
+ specs = {}
677
+ order = []
678
+
679
+ # collect enque signals to setup queue
680
+ signals = queue.to_a.collect do |(node, args)|
681
+ {'sig' => 'enque', 'args' => [var(node)] + args}
682
+ end
683
+
684
+ # collect and trace application objects
685
+ objects.keys.sort_by do |var|
686
+ var.to_s
687
+ end.each do |var|
688
+ obj = objects[var]
689
+ order.concat trace(obj, specs)
690
+ end
691
+
692
+ middleware.each do |obj|
693
+ order.concat trace(obj, specs)
694
+ end
695
+
696
+ if bare
697
+ order.delete(self)
698
+ specs.delete(self)
699
+ else
700
+ order.unshift(self)
701
+ trace(self, specs)
702
+ end
703
+ order.uniq!
704
+
705
+ # assemble specs
706
+ variables = {}
707
+ objects.each_pair do |var, obj|
708
+ (variables[obj] ||= []) << var
709
+ end
710
+
711
+ invert_env = env ? env.invert : nil
712
+ specs.keys.each do |obj|
713
+ spec = {'sig' => 'set'}
714
+
715
+ # assign variables
716
+ if vars = variables[obj]
717
+ if vars.length == 1
718
+ spec['var'] = vars[0]
719
+ else
720
+ spec['var'] = vars
721
+ end
722
+ end
723
+
724
+ # assign the class
725
+ klass = obj.class
726
+ klass = invert_env[klass] if invert_env
727
+ spec['class'] = klass.to_s
728
+
729
+ # merge obj_spec if possible
730
+ obj_spec = specs[obj]
731
+ if (obj_spec.keys & RESERVED_KEYS).empty?
732
+ spec.merge!(obj_spec)
733
+ else
734
+ spec['spec'] = obj_spec
735
+ end
736
+
737
+ specs[obj] = spec
738
+ end
739
+
740
+ middleware.each do |obj|
741
+ spec = specs[obj]
742
+ spec['sig'] = 'use'
743
+ end
744
+
745
+ order.collect! {|obj| specs[obj] }.concat(signals)
380
746
  end
381
747
 
382
- protected
748
+ def to_spec
749
+ signals = serialize(false)
750
+ spec = signals.shift
751
+
752
+ spec.delete('self')
753
+ spec.delete('sig')
754
+
755
+ var = spec.delete('var')
756
+ klass = spec.delete('class')
757
+ spec = spec.delete('spec') || spec
758
+
759
+ signals.unshift(
760
+ 'sig' => 'set',
761
+ 'var' => var,
762
+ 'class' => klass,
763
+ 'self' => true
764
+ ) if var
765
+ spec['signals'] = signals
766
+
767
+ spec
768
+ end
769
+
770
+ def inspect
771
+ "#<#{self.class}:#{object_id} #{info}>"
772
+ end
773
+
774
+ 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
+
783
+ # Traces each object backwards and forwards for node, joins, etc. and adds
784
+ # each to specs as needed. The trace determines and returns the order in
785
+ # which these specs must be initialized to make sense. Circular traces
786
+ # are detected.
787
+ #
788
+ # Note that order should not be provided for the first call; order must be
789
+ # trace-specific. For example (a -> b means 'a' references or requires
790
+ # 'b' so read backwards for order):
791
+ #
792
+ # # Circular trace [a,c,b,a]
793
+ # a -> b -> c -> a
794
+ #
795
+ # # Not a problem [[b,a], [b,c]] => [b,a,c]
796
+ # a -> b
797
+ # c -> b
798
+ #
799
+ def trace(obj, specs, order=[]) # :nodoc:
800
+ if specs.has_key?(obj)
801
+ return order
802
+ end
803
+
804
+ # check the object can be serialized
805
+ unless obj.respond_to?(:to_spec)
806
+ warn "cannot serialize: #{obj}"
807
+ return order
808
+ end
809
+ specs[obj] = obj == self ? self_to_spec : obj.to_spec
810
+
811
+ # trace references; refs must exist before obj and
812
+ # obj must exist before brefs (back-references)
813
+ if obj.respond_to?(:associations)
814
+ refs, brefs = obj.associations
815
+
816
+ refs.each {|ref| trace(ref, specs, order) } if refs
817
+ order << obj
818
+ brefs.each {|bref| trace(bref, specs, order) } if brefs
819
+ else
820
+ order << obj
821
+ end
822
+
823
+ order
824
+ end
825
+
826
+ def self_to_spec # :nodoc:
827
+ config = self.config.to_hash {|hash, key, value| hash[key.to_s] = value }
828
+ {'config' => config, 'self' => true}
829
+ end
383
830
 
384
831
  # TerminateErrors are raised to kill executing nodes when terminate is
385
832
  # called on an running App. They are handled by the run rescue code.