wukong 3.0.1 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +1 -1
- data/README.md +253 -45
- data/bin/wu +34 -0
- data/bin/wu-source +5 -0
- data/examples/Gemfile +0 -1
- data/examples/deploy_pack/Gemfile +0 -1
- data/examples/improver/tweet_summary.rb +73 -0
- data/examples/ruby_project/Gemfile +0 -1
- data/examples/splitter.rb +94 -0
- data/examples/twitter.rb +5 -0
- data/lib/hanuman.rb +1 -1
- data/lib/hanuman/graph.rb +39 -22
- data/lib/hanuman/stage.rb +46 -13
- data/lib/hanuman/tree.rb +67 -0
- data/lib/wukong.rb +6 -1
- data/lib/wukong/dataflow.rb +19 -48
- data/lib/wukong/driver.rb +176 -65
- data/lib/wukong/{local → driver}/event_machine_driver.rb +1 -13
- data/lib/wukong/driver/wiring.rb +68 -0
- data/lib/wukong/local.rb +6 -4
- data/lib/wukong/local/runner.rb +14 -16
- data/lib/wukong/local/stdio_driver.rb +72 -12
- data/lib/wukong/processor.rb +1 -30
- data/lib/wukong/runner.rb +2 -0
- data/lib/wukong/runner/command_runner.rb +44 -0
- data/lib/wukong/source.rb +33 -0
- data/lib/wukong/source/source_driver.rb +74 -0
- data/lib/wukong/source/source_runner.rb +38 -0
- data/lib/wukong/spec_helpers/shared_examples.rb +0 -1
- data/lib/wukong/spec_helpers/unit_tests.rb +6 -5
- data/lib/wukong/spec_helpers/unit_tests/unit_test_driver.rb +4 -14
- data/lib/wukong/spec_helpers/unit_tests/unit_test_runner.rb +7 -8
- data/lib/wukong/version.rb +1 -1
- data/lib/wukong/widget/echo.rb +55 -0
- data/lib/wukong/widget/{processors.rb → extract.rb} +0 -106
- data/lib/wukong/widget/filters.rb +15 -0
- data/lib/wukong/widget/logger.rb +56 -0
- data/lib/wukong/widget/operators.rb +82 -0
- data/lib/wukong/widget/reducers.rb +2 -0
- data/lib/wukong/widget/reducers/improver.rb +71 -0
- data/lib/wukong/widget/reducers/join_xml.rb +37 -0
- data/lib/wukong/widget/serializers.rb +21 -6
- data/lib/wukong/widgets.rb +6 -3
- data/spec/hanuman/graph_spec.rb +73 -10
- data/spec/hanuman/stage_spec.rb +15 -0
- data/spec/hanuman/tree_spec.rb +119 -0
- data/spec/spec_helper.rb +13 -1
- data/spec/support/example_test_helpers.rb +0 -1
- data/spec/support/model_test_helpers.rb +1 -1
- data/spec/support/shared_context_for_graphs.rb +57 -0
- data/spec/support/shared_examples_for_builders.rb +8 -15
- data/spec/wukong/driver_spec.rb +152 -0
- data/spec/wukong/local/runner_spec.rb +1 -12
- data/spec/wukong/local/stdio_driver_spec.rb +73 -0
- data/spec/wukong/processor_spec.rb +0 -1
- data/spec/wukong/runner_spec.rb +2 -2
- data/spec/wukong/source_spec.rb +6 -0
- data/spec/wukong/widget/extract_spec.rb +101 -0
- data/spec/wukong/widget/logger_spec.rb +23 -0
- data/spec/wukong/widget/operators_spec.rb +25 -0
- data/spec/wukong/widget/reducers/join_xml_spec.rb +25 -0
- data/spec/wukong/wu-source_spec.rb +32 -0
- data/spec/wukong/wu_spec.rb +14 -0
- data/wukong.gemspec +1 -2
- metadata +45 -28
- data/lib/wukong/local/tcp_driver.rb +0 -47
- data/spec/wu/geo/geolocated_spec.rb +0 -247
- data/spec/wukong/widget/processors_spec.rb +0 -125
data/lib/wukong.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'configliere'
|
2
|
-
require 'vayacondios-client'
|
3
2
|
require 'multi_json'
|
4
3
|
require 'eventmachine'
|
5
4
|
require 'log4r'
|
@@ -35,6 +34,10 @@ module Wukong
|
|
35
34
|
|
36
35
|
add_shortcut_method_for(:processor, ProcessorBuilder)
|
37
36
|
add_shortcut_method_for(:dataflow, DataflowBuilder)
|
37
|
+
|
38
|
+
def self.doc_helpers_path
|
39
|
+
File.expand_path('../wukong/doc_helpers.rb', __FILE__)
|
40
|
+
end
|
38
41
|
|
39
42
|
end
|
40
43
|
|
@@ -45,5 +48,7 @@ require_relative 'wukong/widgets'
|
|
45
48
|
require_relative 'wukong/local'
|
46
49
|
|
47
50
|
module Wukong
|
51
|
+
|
52
|
+
# Built-in Wukong processors and dataflows.
|
48
53
|
BUILTINS = Set.new(Wukong.registry.show.keys)
|
49
54
|
end
|
data/lib/wukong/dataflow.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
module Wukong
|
2
|
-
|
2
|
+
|
3
|
+
class Dataflow < Hanuman::Tree
|
4
|
+
def self.configure(settings)
|
5
|
+
settings.description = builder.description if builder.description
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class DataflowBuilder < Hanuman::TreeBuilder
|
3
10
|
|
4
11
|
def description desc=nil
|
5
12
|
@description = desc if desc
|
@@ -10,17 +17,20 @@ module Wukong
|
|
10
17
|
|
11
18
|
def handle_dsl_arguments_for(stage, *args, &action)
|
12
19
|
options = args.extract_options!
|
20
|
+
while stages.include?(stage.label)
|
21
|
+
parts = stage.label.to_s.split('_')
|
22
|
+
if parts.last.to_i > 0
|
23
|
+
parts[-1] = parts.last.to_i + 1
|
24
|
+
else
|
25
|
+
parts.push(1)
|
26
|
+
end
|
27
|
+
stage.label = parts.map(&:to_s).join('_').to_sym
|
28
|
+
end
|
13
29
|
stage.merge!(options.merge(action: action).compact)
|
14
|
-
stage
|
30
|
+
stage.graph = self
|
31
|
+
stage
|
15
32
|
end
|
16
33
|
|
17
|
-
def linkable_name(direction)
|
18
|
-
case direction
|
19
|
-
when :in then directed_sort.first
|
20
|
-
when :out then directed_sort.last
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
34
|
def method_missing(name, *args, &blk)
|
25
35
|
if stages[name]
|
26
36
|
handle_dsl_arguments_for(stages[name], *args, &blk)
|
@@ -30,43 +40,4 @@ module Wukong
|
|
30
40
|
end
|
31
41
|
|
32
42
|
end
|
33
|
-
|
34
|
-
class Dataflow < Hanuman::Graph
|
35
|
-
|
36
|
-
def self.description desc=nil
|
37
|
-
@description = desc if desc
|
38
|
-
@description
|
39
|
-
end
|
40
|
-
|
41
|
-
def has_input?(stage)
|
42
|
-
links.any?{ |link| link.into == stage }
|
43
|
-
end
|
44
|
-
|
45
|
-
def has_output?(stage)
|
46
|
-
links.any?{ |link| link.from == stage }
|
47
|
-
end
|
48
|
-
|
49
|
-
def connected?(stage)
|
50
|
-
input = has_input?(stage) || stages[stage].is_a?(Wukong::Source)
|
51
|
-
output = has_output?(stage) || stages[stage].is_a?(Wukong::Sink)
|
52
|
-
input && output
|
53
|
-
end
|
54
|
-
|
55
|
-
def complete?
|
56
|
-
stages.all?{ |(name, stage)| connected? name }
|
57
|
-
end
|
58
|
-
|
59
|
-
def setup
|
60
|
-
directed_sort.each{ |name| stages[name].setup }
|
61
|
-
end
|
62
|
-
|
63
|
-
def run
|
64
|
-
stages[directed_sort.first].run
|
65
|
-
end
|
66
|
-
|
67
|
-
def stop
|
68
|
-
directed_sort.each{ |name| stages[name].stop }
|
69
|
-
end
|
70
|
-
|
71
|
-
end
|
72
43
|
end
|
data/lib/wukong/driver.rb
CHANGED
@@ -1,103 +1,214 @@
|
|
1
|
+
require_relative('driver/wiring')
|
2
|
+
|
1
3
|
module Wukong
|
4
|
+
|
5
|
+
# A Driver is a class including the DriverMethods module which
|
6
|
+
# connects a Dataflow or Processor to the external world of inputs
|
7
|
+
# and outputs.
|
8
|
+
#
|
9
|
+
# @example Minimal Driver class
|
10
|
+
#
|
11
|
+
# class MinimalDriver
|
12
|
+
# include Wukong::DriverMethods
|
13
|
+
# def initialize(label, settings)
|
14
|
+
# construct_dataflow(label, settings)
|
15
|
+
# end
|
16
|
+
# def process record
|
17
|
+
# puts record
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# The MinimalDriver#send_through_dataflow method can be called on an
|
22
|
+
# instance of MinimalDriver with any input record.
|
23
|
+
#
|
24
|
+
# This record will be passed through the dataflow, starting from its
|
25
|
+
# root, and each record yielded at the leaves of the dataflow will
|
26
|
+
# be passed to the driver's #process method.
|
27
|
+
#
|
28
|
+
# The #process method of an implementing driver should *not* yield,
|
29
|
+
# unlike the process method of a Processor class. Instead, it
|
30
|
+
# should treat its argument as an output of the dataflow and do
|
31
|
+
# something appropriate to the driver (write to file, database,
|
32
|
+
# terminal, &c.).
|
33
|
+
#
|
34
|
+
# Drivers are also responsible for implementing the lifecycle of
|
35
|
+
# processors and dataflows they drive. A more complete version of
|
36
|
+
# the above driver class would:
|
37
|
+
#
|
38
|
+
# * call the #setup_dataflow method when ready to trigger the
|
39
|
+
# Processor#setup method on each processor in the dataflow
|
40
|
+
#
|
41
|
+
# * call the #finalize_dataflow method when indicating that the
|
42
|
+
# dataflow should consider a batch of records complete
|
43
|
+
#
|
44
|
+
# * call the #finalize_and_stop_dataflow method to indicate the
|
45
|
+
# last batch of records and to trigger the Processor#stop method
|
46
|
+
# on each processor in the dataflow
|
47
|
+
#
|
48
|
+
# Driver instances are started by Runners which should delegate to
|
49
|
+
# the `start` method driver class itself.
|
50
|
+
#
|
51
|
+
# @see Wukong::Local::StdioDriver for a complete example of a driver.
|
52
|
+
# @see Wukong::Local::Runner for an example of how runners call drivers.
|
2
53
|
module DriverMethods
|
3
54
|
|
4
|
-
attr_accessor :
|
5
|
-
|
55
|
+
attr_accessor :label
|
6
56
|
attr_accessor :settings
|
57
|
+
attr_accessor :dataflow
|
7
58
|
|
8
|
-
|
9
|
-
|
59
|
+
# Classes including DriverMethods should override this method with
|
60
|
+
# some way of handling the `output_record` that is appropriate for
|
61
|
+
# the driver.
|
62
|
+
#
|
63
|
+
# @param [Object] output_record
|
64
|
+
def process output_record
|
65
|
+
raise NotImplementedError.new("Define the #{self.class}#process method to handle output records from the dataflow")
|
10
66
|
end
|
11
67
|
|
12
|
-
|
13
|
-
|
14
|
-
|
68
|
+
# Construct a dataflow from the given `label` and `settings`.
|
69
|
+
#
|
70
|
+
# This method does **not** cause Processor#setup to be called on
|
71
|
+
# any of the processors in the dataflow. Call the #setup_dataflow
|
72
|
+
# method to explicitly have setup occur. This distinction is
|
73
|
+
# useful for drivers which themselves need to do complex
|
74
|
+
# initialization before letting processors in the dataflow
|
75
|
+
# initialize.
|
76
|
+
#
|
77
|
+
# @param [Symbol] label the name of the dataflow (or processor) to build
|
78
|
+
# @param [Hash] settings
|
79
|
+
# @param settings [String] :to Serialize all output via the named serializer (json, tsv)
|
80
|
+
# @param settings [String] :from Deserialize all input via the named deserializer (json, tsv)
|
81
|
+
# @param settings [String] :as Recordize each input as instances of the given class
|
82
|
+
#
|
83
|
+
# @see #setup_dataflow
|
84
|
+
def construct_dataflow(label, settings={})
|
85
|
+
self.label = label
|
86
|
+
self.settings = settings
|
87
|
+
prepend(:recordize) if settings[:as]
|
88
|
+
prepend("from_#{settings[:from]}".to_sym) if settings[:from]
|
89
|
+
append("to_#{settings[:to]}".to_sym) if settings[:to]
|
90
|
+
build_dataflow
|
15
91
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def build_serializer(direction, label, options)
|
22
|
-
lookup_and_build("#{direction}_#{label}", options)
|
92
|
+
|
93
|
+
# Set up this driver. Called before setting up any of the
|
94
|
+
# dataflow stages.
|
95
|
+
def setup
|
23
96
|
end
|
24
97
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
98
|
+
# Walks the dataflow and calls Processor#setup on each of the
|
99
|
+
# processors.
|
100
|
+
def setup_dataflow
|
101
|
+
setup
|
102
|
+
dataflow.each_stage do |stage|
|
103
|
+
stage.setup
|
29
104
|
end
|
30
105
|
end
|
31
106
|
|
32
|
-
|
33
|
-
|
107
|
+
# Send the given `record` through the dataflow.
|
108
|
+
#
|
109
|
+
# @param [Object] record
|
110
|
+
def send_through_dataflow(record)
|
111
|
+
wiring.start_with(dataflow.root).call(record)
|
34
112
|
end
|
35
113
|
|
114
|
+
# Perform finalization code for this driver. Runs after #setup
|
115
|
+
# and before #stop.
|
116
|
+
def finalize
|
117
|
+
end
|
118
|
+
|
119
|
+
# Indicate a full batch of records has already been sent through
|
120
|
+
# and any batch-oriented or accumulative operations should trigger
|
121
|
+
# (e.g. - counting).
|
122
|
+
#
|
123
|
+
# Walks the dataflow calling Processor#finalize on each processor.
|
124
|
+
#
|
125
|
+
# On the *last* batch, the #finalize_and_stop_dataflow method
|
126
|
+
# should be called instead.
|
127
|
+
#
|
128
|
+
# @see #finalize_and_stop_dataflow
|
36
129
|
def finalize_dataflow
|
37
|
-
|
38
|
-
|
130
|
+
finalize
|
131
|
+
dataflow.each_stage do |stage|
|
132
|
+
stage.finalize(&wiring.advance(stage))
|
39
133
|
end
|
40
134
|
end
|
41
135
|
|
136
|
+
# Works similar to #finalize_dataflow but calls Processor#stop
|
137
|
+
# after calling Processor#finalize on each processor.
|
42
138
|
def finalize_and_stop_dataflow
|
43
|
-
|
44
|
-
|
139
|
+
finalize
|
140
|
+
dataflow.each_stage do |stage|
|
141
|
+
stage.finalize(&wiring.advance(stage))
|
45
142
|
stage.stop
|
46
|
-
end
|
143
|
+
end
|
144
|
+
stop
|
47
145
|
end
|
48
146
|
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
expected_input_model = (options[:consumes].constantize rescue nil) || dataflow.first.expected_record_type(:consumes)
|
54
|
-
dataflow.unshift lookup_and_build(:recordize, model: expected_input_model) if expected_input_model
|
55
|
-
expected_output_model = (options[:produces].constantize rescue nil) || dataflow.first.expected_record_type(:produces)
|
56
|
-
dataflow.push lookup_and_build(:recordize, model: expected_output_model) if expected_output_model
|
57
|
-
expected_input_serialization = options[:from] || dataflow.last.expected_serialization(:from)
|
58
|
-
add_serialization(dataflow, :from, expected_input_serialization, options) if expected_input_serialization
|
59
|
-
expected_output_serialization = options[:to] || dataflow.last.expected_serialization(:to)
|
60
|
-
add_serialization(dataflow, :to, expected_output_serialization, options) if expected_output_serialization
|
61
|
-
dataflow.push self
|
62
|
-
end
|
63
|
-
end
|
147
|
+
# Perform shutdown code for this driver. Called after #finalize
|
148
|
+
# and after all stages have been finalized and stopped.
|
149
|
+
def stop
|
150
|
+
end
|
64
151
|
|
65
|
-
|
66
|
-
attr_accessor :dataflow
|
152
|
+
protected
|
67
153
|
|
68
|
-
|
69
|
-
|
154
|
+
# The builder for this driver's `label`, either for a Processor or
|
155
|
+
# a Dataflow.
|
156
|
+
#
|
157
|
+
# @return [Wukong::ProcessorBuilder, Wukong::DataflowBuilder]
|
158
|
+
def builder
|
159
|
+
return @builder if @builder
|
160
|
+
raise Wukong::Error.new("could not find definition for <#{label}>") unless Wukong.registry.registered?(label.to_sym)
|
161
|
+
@builder = Wukong.registry.retrieve(label.to_sym)
|
70
162
|
end
|
71
163
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
164
|
+
# Return the builder for this driver's dataflow.
|
165
|
+
#
|
166
|
+
# Even if a Processor was originally named by this driver's
|
167
|
+
# `label`, a DataflowBuilder will be returned here. The
|
168
|
+
# DataflowBuilder is itself built from just the ProcessorBuilder
|
169
|
+
# alone.
|
170
|
+
#
|
171
|
+
# @return [Wukong::DataflowBuilder]
|
172
|
+
# @see #builder
|
173
|
+
def dataflow_builder
|
174
|
+
@dataflow_builder ||= (builder.is_a?(DataflowBuilder) ? builder : Wukong::DataflowBuilder.receive(for_class: Class.new(Wukong::Dataflow), stages: {label.to_sym => builder}))
|
77
175
|
end
|
78
176
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
def
|
84
|
-
|
177
|
+
# Build the dataflow using the #dataflow_builder and the supplied
|
178
|
+
# `settings`.
|
179
|
+
#
|
180
|
+
# @return [Wukong::Dataflow]
|
181
|
+
def build_dataflow
|
182
|
+
self.dataflow = dataflow_builder.build(settings)
|
85
183
|
end
|
86
184
|
|
87
|
-
|
88
|
-
|
89
|
-
|
185
|
+
# Add the processor with the given `new_label` in front of this
|
186
|
+
# driver's dataflow, making it into the new root of the dataflow.
|
187
|
+
#
|
188
|
+
# @param [Symbol] new_label
|
189
|
+
def prepend new_label
|
190
|
+
raise Wukong::Error.new("could not find processor <#{new_label}> to prepend") unless Wukong.registry.registered?(new_label)
|
191
|
+
dataflow_builder.prepend(Wukong.registry.retrieve(new_label))
|
90
192
|
end
|
91
193
|
|
92
|
-
#
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
194
|
+
# Add the processor with the given `new_label` at the end of each
|
195
|
+
# of this driver's dataflow's leaves.
|
196
|
+
#
|
197
|
+
# @param [Symbol] new_label
|
198
|
+
def append new_label
|
199
|
+
raise Wukong::Error.new("could not find processor <#{new_label}> to append") unless Wukong.registry.registered?(new_label)
|
200
|
+
dataflow_builder.append(Wukong.registry.retrieve(new_label))
|
201
|
+
end
|
97
202
|
|
98
|
-
|
99
|
-
|
203
|
+
# Returns the underlying Wiring object that will coordinate
|
204
|
+
# transfer of records from the driver to the dataflow and back to
|
205
|
+
# the driver.
|
206
|
+
#
|
207
|
+
# @return [Wiring]
|
208
|
+
def wiring
|
209
|
+
@wiring ||= Wiring.new(self, dataflow)
|
100
210
|
end
|
101
211
|
|
102
212
|
end
|
213
|
+
|
103
214
|
end
|
@@ -1,12 +1,7 @@
|
|
1
1
|
module Wukong
|
2
|
-
|
3
|
-
# A module which can be included by other drivers which lets them
|
4
|
-
# use EventMachine under the hood.
|
5
2
|
module EventMachineDriver
|
6
|
-
|
7
3
|
include DriverMethods
|
8
4
|
|
9
|
-
# :nodoc:
|
10
5
|
def self.included klass
|
11
6
|
klass.class_eval do
|
12
7
|
def self.add_signal_traps
|
@@ -15,13 +10,6 @@ module Wukong
|
|
15
10
|
end
|
16
11
|
end
|
17
12
|
end
|
18
|
-
|
19
|
-
# :nodoc:
|
20
|
-
def initialize(label, settings)
|
21
|
-
super
|
22
|
-
@settings = settings
|
23
|
-
@dataflow = construct_dataflow(label, settings)
|
24
|
-
end
|
25
|
-
|
13
|
+
|
26
14
|
end
|
27
15
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Wukong
|
2
|
+
|
3
|
+
# Provides a very Ruby-minded way of walking a dataflow connected to
|
4
|
+
# a driver.
|
5
|
+
class Wiring
|
6
|
+
|
7
|
+
# The driver instance that likely calls the #start_with method and
|
8
|
+
# provides a #process method to be called by this wiring.
|
9
|
+
attr_accessor :driver
|
10
|
+
|
11
|
+
# The dataflow being wired.
|
12
|
+
attr_accessor :dataflow
|
13
|
+
|
14
|
+
# Construct a new Wiring for the given `driver` and `dataflow`.
|
15
|
+
#
|
16
|
+
# @param [#process] driver
|
17
|
+
# @param [Wukong::Dataflow] dataflow
|
18
|
+
def initialize(driver, dataflow)
|
19
|
+
@driver = driver
|
20
|
+
@dataflow = dataflow
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return a proc which, if called with a record, will process that
|
24
|
+
# record through each of the given `stages` as well as through the
|
25
|
+
# rest of the dataflow ahead of them.
|
26
|
+
#
|
27
|
+
# @param [Array<Wukong::Stage>] stages
|
28
|
+
# @return [Proc]
|
29
|
+
def start_with(*stages)
|
30
|
+
to_proc.curry.call(stages)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return a proc (the output of #start_with) which will process
|
34
|
+
# records through the stages that are ahead of the given stage.
|
35
|
+
#
|
36
|
+
# @param [Wukong::Stage] stage
|
37
|
+
# @return [Proc]
|
38
|
+
#
|
39
|
+
# @see #start_with
|
40
|
+
def advance(stage)
|
41
|
+
# This is where the tree of procs will terminate, but only after
|
42
|
+
# having passed all output records through the driver -- the
|
43
|
+
# last "stage".
|
44
|
+
return start_with() if stage.nil? || stage == driver
|
45
|
+
|
46
|
+
# Otherwise we're still in the middle of the tree...
|
47
|
+
descendents = dataflow.descendents(stage)
|
48
|
+
if descendents.empty?
|
49
|
+
# No descendents it means we've reached a leaf of the tree so
|
50
|
+
# we'll run records through the driver to generate output.
|
51
|
+
start_with(driver)
|
52
|
+
else
|
53
|
+
# Otherwise continue down the tree of procs...
|
54
|
+
start_with(*descendents)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# :nodoc:
|
59
|
+
def to_proc
|
60
|
+
return @wiring if @wiring
|
61
|
+
@wiring = Proc.new do |stages, record|
|
62
|
+
stages.each do |stage|
|
63
|
+
stage.process(record, &advance(stage)) if stage
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|