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/History +21 -0
- data/MIT-LICENSE +17 -15
- data/README +13 -30
- data/bin/tap +19 -24
- data/cmd/console.rb +1 -12
- data/cmd/manifest.rb +14 -19
- data/cmd/run.rb +96 -86
- data/doc/API +194 -54
- data/doc/Examples/Command Line +27 -1
- data/lib/tap.rb +2 -1
- data/lib/tap/app.rb +613 -166
- data/lib/tap/app/api.rb +115 -0
- data/lib/tap/app/queue.rb +36 -37
- data/lib/tap/app/state.rb +2 -1
- data/lib/tap/env.rb +454 -270
- data/lib/tap/env/constant.rb +83 -33
- data/lib/tap/env/context.rb +61 -0
- data/lib/tap/env/manifest.rb +140 -50
- data/lib/tap/env/minimap.rb +55 -39
- data/lib/tap/join.rb +71 -53
- data/lib/tap/joins/sync.rb +3 -1
- data/lib/tap/middleware.rb +4 -25
- data/lib/tap/middlewares/debugger.rb +75 -0
- data/lib/tap/parser.rb +268 -0
- data/lib/tap/prompt.rb +36 -0
- data/lib/tap/root.rb +3 -3
- data/lib/tap/signals.rb +26 -0
- data/lib/tap/signals/class_methods.rb +222 -0
- data/lib/tap/signals/help.rb +40 -0
- data/lib/tap/signals/module_methods.rb +20 -0
- data/lib/tap/signals/signal.rb +68 -0
- data/lib/tap/task.rb +28 -79
- data/lib/tap/tasks/dump.rb +6 -0
- data/lib/tap/tasks/load.rb +9 -37
- data/lib/tap/templater.rb +12 -1
- data/lib/tap/version.rb +1 -1
- metadata +22 -16
- data/doc/Class Reference +0 -330
- data/lib/tap/exe.rb +0 -130
- data/lib/tap/schema.rb +0 -374
- data/lib/tap/schema/parser.rb +0 -425
- data/lib/tap/schema/utils.rb +0 -56
data/doc/API
CHANGED
@@ -1,83 +1,223 @@
|
|
1
1
|
= Application Programming Interface
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
The
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
131
|
+
=== Spec References
|
29
132
|
|
30
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
175
|
+
=== Resource Identifiers
|
49
176
|
|
50
|
-
|
51
|
-
|
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
|
-
|
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
|
-
|
61
|
-
|
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
|
-
[
|
197
|
+
[a.rb]
|
198
|
+
# Sample::a
|
199
|
+
class Sample
|
200
|
+
end
|
64
201
|
|
65
|
-
|
66
|
-
|
202
|
+
[b.rb]
|
203
|
+
require 'a'
|
204
|
+
|
205
|
+
# Sample::b
|
67
206
|
class Sample
|
68
207
|
end
|
69
208
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
82
|
-
|
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.
|
data/doc/Examples/Command Line
CHANGED
@@ -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
data/lib/tap/app.rb
CHANGED
@@ -1,98 +1,17 @@
|
|
1
1
|
require 'logger'
|
2
|
-
require '
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
131
|
-
attr_reader :
|
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
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
98
|
+
nest :env, Env, # The application environment
|
99
|
+
:type => :hidden,
|
100
|
+
:writer => false,
|
101
|
+
:init => false
|
143
102
|
|
144
|
-
#
|
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
|
-
@
|
152
|
-
@
|
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
|
-
#
|
160
|
-
|
161
|
-
|
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
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
#
|
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
|
-
|
177
|
-
|
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
|
-
|
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
|
221
|
-
# is used. Reset raises an error unless state
|
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
|
-
|
229
|
-
|
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
|
-
#
|
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
|
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
|
523
|
+
# Dispatch returns the stack result.
|
246
524
|
def dispatch(node, inputs=[])
|
247
525
|
result = stack.call(node, inputs)
|
248
526
|
|
249
|
-
|
250
|
-
|
251
|
-
|
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.
|
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
|
264
|
-
#
|
265
|
-
#
|
266
|
-
# continute to completion.
|
267
|
-
#
|
268
|
-
#
|
269
|
-
#
|
270
|
-
#
|
271
|
-
# Calls to run when the state is not
|
272
|
-
#
|
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
|
296
|
-
#
|
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
|
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
|
-
#
|
306
|
-
#
|
307
|
-
#
|
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
|
312
|
-
# as stop.
|
597
|
+
# it will continue to completion.
|
313
598
|
#
|
314
|
-
# Does nothing if state
|
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
|
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 ==
|
324
|
-
yield if block_given?
|
325
|
-
raise
|
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.
|
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
|
-
#
|
376
|
-
#
|
377
|
-
|
378
|
-
|
379
|
-
|
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
|
-
|
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.
|