nxt_pipeline 0.4.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -1
- data/Gemfile.lock +42 -44
- data/README.md +364 -108
- data/lib/nxt_pipeline/callbacks.rb +61 -0
- data/lib/nxt_pipeline/error_callback.rb +5 -2
- data/lib/nxt_pipeline/pipeline.rb +175 -68
- data/lib/nxt_pipeline/step.rb +105 -40
- data/lib/nxt_pipeline/version.rb +1 -1
- data/lib/nxt_pipeline.rb +40 -1
- data/nxt_pipeline.gemspec +1 -0
- metadata +21 -7
- data/lib/nxt_pipeline/constructor.rb +0 -20
data/README.md
CHANGED
@@ -2,7 +2,17 @@
|
|
2
2
|
|
3
3
|
# NxtPipeline
|
4
4
|
|
5
|
-
|
5
|
+
NxtPipeline is an orchestration framework for your service objects or function objects, how I like to call them.
|
6
|
+
Service objects are a very wide spread way of organizing code in the Ruby and Rails communities. Since it's little classes
|
7
|
+
doing one thing you can think of them as function objects and thus they often share a common interface in a project.
|
8
|
+
There are also many frameworks out there that normalize the usage of service objects and provide a specific way
|
9
|
+
of writing service objects and often also allow to orchestrate (reduce) these service objects.
|
10
|
+
Compare [light-service](https://github.com/adomokos/light-service) for instance.
|
11
|
+
|
12
|
+
The idea of NxtPipeline was to build a flexible orchestration framework for service objects without them having to conform
|
13
|
+
to a specific interface. Instead NxtPipeline expects you to specify how to execute different kinds of service objects
|
14
|
+
through so called constructors and thereby does not dictate you how to write your service objects. Nevertheless this still
|
15
|
+
mostly makes sense if your service objects share common interfaces to keep the necessary configuration to a minimum.
|
6
16
|
|
7
17
|
## Installation
|
8
18
|
|
@@ -22,186 +32,432 @@ Or install it yourself as:
|
|
22
32
|
|
23
33
|
## Usage
|
24
34
|
|
25
|
-
###
|
35
|
+
### Example
|
26
36
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
by the step yielded to the constructor.
|
37
|
+
Let's look at an example. Here validator service objects are orchestrated with NxtPipeline to build a validation
|
38
|
+
pipeline. We inject the accumulator `{ value: 'aki', errors: [] }` that is then passed through all validation steps.
|
39
|
+
If an validator returns an error it's added to the array of errors of the accumulator to collect all errors of all steps.
|
31
40
|
|
32
41
|
```ruby
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
42
|
+
class Validator
|
43
|
+
attr_accessor :error
|
44
|
+
end
|
45
|
+
|
46
|
+
class TypeChecker < Validator
|
47
|
+
def initialize(value, type:)
|
48
|
+
@value = value
|
49
|
+
@type = type
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_reader :value, :type
|
53
|
+
|
54
|
+
def call
|
55
|
+
return if value.is_a?(type)
|
56
|
+
self.error = "Value does not match type #{type}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class MinSize < Validator
|
61
|
+
def initialize(value, size:)
|
62
|
+
@value = value
|
63
|
+
@size = size
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_reader :value, :size
|
67
|
+
|
68
|
+
def call
|
69
|
+
return if value.size >= size
|
70
|
+
self.error = "Value size must be greater than #{size-1}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class MaxSize < Validator
|
75
|
+
def initialize(value, size:)
|
76
|
+
@value = value
|
77
|
+
@size = size
|
41
78
|
end
|
42
|
-
|
43
|
-
|
44
|
-
|
79
|
+
|
80
|
+
attr_reader :value, :size
|
81
|
+
|
82
|
+
def call
|
83
|
+
return if value.size <= size
|
84
|
+
self.error = "Value size must be less than #{size+1}"
|
45
85
|
end
|
46
86
|
end
|
47
87
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
88
|
+
class Uniqueness < Validator
|
89
|
+
def initialize(value, scope:)
|
90
|
+
@value = value
|
91
|
+
@scope = scope
|
92
|
+
end
|
93
|
+
|
94
|
+
attr_reader :value, :scope
|
95
|
+
|
96
|
+
def call
|
97
|
+
return if scope.count { |item| item == value }
|
98
|
+
self.error = "Value is not unique in: #{scope}"
|
99
|
+
end
|
52
100
|
end
|
53
101
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
102
|
+
result = NxtPipeline.call({ value: 'aki', errors: [] }) do |p|
|
103
|
+
p.constructor(:validator, default: true) do |acc, step|
|
104
|
+
validator = step.argument.new(acc.fetch(:value), **step.options)
|
105
|
+
validator.call
|
106
|
+
acc[:errors] << validator.error if validator.error.present?
|
107
|
+
|
108
|
+
acc
|
109
|
+
end
|
110
|
+
|
111
|
+
p.step TypeChecker, options: { type: String }
|
112
|
+
p.step MinSize, options: { size: 4 }
|
113
|
+
p.step MaxSize, options: { size: 10 }
|
114
|
+
p.step Uniqueness, options: { scope: ['andy', 'aki', 'lütfi', 'rapha'] }
|
61
115
|
end
|
116
|
+
|
117
|
+
result # => { value: 'aki', errors: ['Value size must be greater than 3'] }
|
62
118
|
```
|
63
119
|
|
64
|
-
###
|
120
|
+
### Constructors
|
121
|
+
|
122
|
+
In order to reduce over your service objects you have to define constructors so that the pipeline knows how to execute
|
123
|
+
a specific step. You can define constructors globally and specific to a pipeline.
|
65
124
|
|
66
|
-
|
125
|
+
Make a constructor available for all pipelines of your project by defining it globally with:
|
67
126
|
|
68
127
|
```ruby
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
pipeline.step :job, job_class: MyOtherJobClass
|
128
|
+
NxtPipeline.constructor(:service) do |acc, step|
|
129
|
+
validator = step.argument.new(acc.fetch(:value), **step.options)
|
130
|
+
validator.call
|
131
|
+
acc[:errors] << validator.error if validator.error.present?
|
74
132
|
|
75
|
-
|
76
|
-
# ...
|
133
|
+
acc
|
77
134
|
end
|
135
|
+
```
|
136
|
+
|
137
|
+
Or define a constructor only locally for a specific pipeline.
|
78
138
|
|
79
|
-
|
80
|
-
|
139
|
+
```ruby
|
140
|
+
NxtPipeline.new({ value: 'aki', errors: [] }) do |p|
|
141
|
+
p.constructor(:validator, default: true) do |acc, step|
|
142
|
+
validator = step.argument.new(acc.fetch(:value), **step.options)
|
143
|
+
validator.call
|
144
|
+
acc[:errors] << validator.error if validator.error.present?
|
145
|
+
|
146
|
+
acc
|
147
|
+
end
|
148
|
+
|
149
|
+
p.step TypeChecker, options: { type: String }
|
150
|
+
# ...
|
81
151
|
end
|
82
152
|
```
|
83
153
|
|
84
|
-
|
85
|
-
will be used as :to_s option per default. When no type was given for an inline block the type of the inline block
|
86
|
-
will be set to :inline.
|
87
|
-
|
88
|
-
### Execution
|
154
|
+
Constructor Hierarchy
|
89
155
|
|
90
|
-
|
156
|
+
In order to execute a specific step the pipeline firstly checks whether a constructor was specified for a step:
|
157
|
+
`pipeline.step MyServiceClass, constructor: :service`. If this is not the case it checks whether there is a resolver
|
158
|
+
registered that applies. If that's not the case the pipeline checks if there is a constructor registered for the
|
159
|
+
argument that was passed in. This means if you register constructors directly for the arguments you pass in you don't
|
160
|
+
have to specify this constructor option. Therefore the following would work without the need to provide a constructor
|
161
|
+
for the steps.
|
91
162
|
|
92
163
|
```ruby
|
93
|
-
|
164
|
+
NxtPipeline.new({}) do |p|
|
165
|
+
p.constructor(:service) do |acc, step|
|
166
|
+
step.service_class.new(acc).call
|
167
|
+
end
|
94
168
|
|
95
|
-
|
169
|
+
p.step :service, service_class: MyServiceClass
|
170
|
+
p.step :service, service_class: MyOtherServiceClass
|
171
|
+
# ...
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
Lastly if no constructor could be resolved directly from the step argument, the pipelines falls back to the locally
|
176
|
+
and then to the globally defined default constructors.
|
96
177
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
178
|
+
### Defining steps
|
179
|
+
|
180
|
+
Once your pipeline knows how to execute your steps you can add those. The `pipeline.step` method expects at least one
|
181
|
+
argument which you can access in the constructor through `step.argument`. You can also pass in additional options
|
182
|
+
that you can access through readers of a step. The `constructor:` option defines which constructor to use for a step
|
183
|
+
where as you can name a step with the `to_s:` option.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
# explicitly define which constructor to use
|
187
|
+
pipeline.step MyServiceClass, constructor: :service
|
188
|
+
# use a block as inline constructor
|
189
|
+
pipeline.step SpecialService, constructor: ->(step, arg:) { step.argument.call(arg: arg) }
|
190
|
+
# Rely on the default constructor
|
191
|
+
pipeline.step MyOtherServiceClass
|
192
|
+
# Define a step name
|
193
|
+
pipeline.step MyOtherServiceClass, to_s: 'First Step'
|
194
|
+
# Or simply execute a (named) block - NO NEED TO DEFINE A CONSTRUCTOR HERE
|
195
|
+
pipeline.step :step_name_for_better_log do |acc, step|
|
196
|
+
# ...
|
102
197
|
end
|
198
|
+
```
|
103
199
|
|
200
|
+
Defining multiple steps at once. This is especially useful to dynamically configure a pipeline for execution and
|
201
|
+
can potentially even come from a yaml configuration or from the database.
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
pipeline.steps([
|
205
|
+
[MyServiceClass, constructor: :service],
|
206
|
+
[MyOtherServiceClass, constructor: :service],
|
207
|
+
[MyJobClass, constructor: :job]
|
208
|
+
])
|
209
|
+
|
210
|
+
# You can also overwrite the steps of a pipeline through explicitly setting them. This will remove any previously
|
211
|
+
# defined steps.
|
212
|
+
pipeline.steps = [
|
213
|
+
[MyServiceClass, constructor: :service],
|
214
|
+
[MyOtherServiceClass, constructor: :service]
|
215
|
+
]
|
104
216
|
```
|
105
217
|
|
106
|
-
|
218
|
+
### Execution
|
219
|
+
|
220
|
+
Once a pipeline contains steps you can call it with `call(accumulator)` whereas it expects you to inject the accumulator
|
221
|
+
as argument that is then passed through all steps.
|
107
222
|
|
108
223
|
```ruby
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
224
|
+
pipeline.call(arg: 'initial argument')
|
225
|
+
|
226
|
+
# Or directly pass the steps you want to execute:
|
227
|
+
pipeline.call(arg: 'initial argument') do |p|
|
228
|
+
p.step MyServiceClass, to_s: 'First step'
|
229
|
+
p.step MyOtherServiceClass, to_s: 'Second step'
|
230
|
+
p.step MyJobClass, constructor: :job
|
231
|
+
p.step MyOtherJobClass, constructor: :job
|
113
232
|
end
|
114
|
-
```
|
233
|
+
```
|
115
234
|
|
116
|
-
You can
|
117
|
-
an interface to it's type, options, status (:success, :skipped, :failed), result, error and the index in the pipeline.
|
235
|
+
You can also create a new instance of a pipeline and directly run it with `call`:
|
118
236
|
|
237
|
+
```ruby
|
238
|
+
NxtPipeline.call(arg: 'initial argument') do |p|
|
239
|
+
p.steps # ...
|
240
|
+
end
|
119
241
|
```
|
120
|
-
pipeline.steps.first
|
121
|
-
# will give you something like this:
|
122
242
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
```
|
243
|
+
You can query the steps of your pipeline simply by calling `pipeline.steps`. A NxtPipeline::Step will provide you with
|
244
|
+
an interface for options, status, execution_finished_at execution_started_at,
|
245
|
+
execution_duration, result, error and the index in the pipeline.
|
246
|
+
|
247
|
+
```
|
248
|
+
pipeline.steps.first
|
249
|
+
# will give you a step object
|
250
|
+
#<NxtPipeline::Step:0x00007f83eb399448...>
|
251
|
+
```
|
133
252
|
|
134
253
|
### Guard clauses
|
135
254
|
|
136
255
|
You can also define guard clauses that take a proc to prevent the execution of a step.
|
137
|
-
|
256
|
+
A guard can accept the change set and the step as arguments.
|
138
257
|
|
139
258
|
```ruby
|
140
|
-
pipeline.
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
259
|
+
pipeline.call('initial argument') do |p|
|
260
|
+
p.step MyServiceClass, if: -> (acc, step) { acc == 'initial argument' }
|
261
|
+
p.step MyOtherServiceClass, unless: -> { false }
|
262
|
+
end
|
263
|
+
|
145
264
|
```
|
146
265
|
|
147
266
|
### Error callbacks
|
148
267
|
|
149
|
-
Apart from defining constructors and steps you can also define error callbacks.
|
268
|
+
Apart from defining constructors and steps you can also define error callbacks. Error callbacks can accept up to
|
269
|
+
three arguments: `error, acc, step`.
|
150
270
|
|
151
271
|
```ruby
|
152
|
-
NxtPipeline
|
153
|
-
p.step
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
p.on_error MyCustomError do |step, opts, error|
|
272
|
+
NxtPipeline.new do |p|
|
273
|
+
p.step # ...
|
274
|
+
|
275
|
+
p.on_error MyCustomError do |error|
|
158
276
|
# First matching error callback will be executed!
|
159
277
|
end
|
160
|
-
|
161
|
-
p.on_errors ArgumentError, KeyError do |
|
278
|
+
|
279
|
+
p.on_errors ArgumentError, KeyError do |error, acc|
|
162
280
|
# First matching error callback will be executed!
|
163
281
|
end
|
164
|
-
|
165
|
-
p.on_errors YetAnotherError, halt_on_error: false do |
|
282
|
+
|
283
|
+
p.on_errors YetAnotherError, halt_on_error: false do |error, acc, step|
|
166
284
|
# After executing the callback the pipeline will not halt but continue to
|
167
285
|
# execute the next steps.
|
168
286
|
end
|
169
|
-
|
170
|
-
p.on_errors do |
|
287
|
+
|
288
|
+
p.on_errors do |error, acc, step|
|
171
289
|
# This will match all errors inheriting from StandardError
|
172
290
|
end
|
173
291
|
end
|
174
|
-
```
|
292
|
+
```
|
293
|
+
|
294
|
+
### Before, around and after callbacks
|
295
|
+
|
296
|
+
You can also define callbacks :before, :around and :after each step and or the `#execute` method. You can also register
|
297
|
+
multiple callbacks, but probably you want to keep them to a minimum to not end up in hell. Also note that before and
|
298
|
+
after callbacks will run even if a step was skipped through a guard clause.
|
175
299
|
|
176
|
-
|
300
|
+
#### Step callbacks
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
NxtPipeline.new do |p|
|
304
|
+
p.before_step do |_, change_set|
|
305
|
+
change_set[:acc] << 'before step 1'
|
306
|
+
change_set
|
307
|
+
end
|
308
|
+
|
309
|
+
p.around_step do |_, change_set, execution|
|
310
|
+
change_set[:acc] << 'around step 1'
|
311
|
+
execution.call # you have to specify where in your callback you want to call the inner block
|
312
|
+
change_set[:acc] << 'around step 1'
|
313
|
+
change_set
|
314
|
+
end
|
315
|
+
|
316
|
+
p.after_step do |_, change_set|
|
317
|
+
change_set[:acc] << 'after step 1'
|
318
|
+
change_set
|
319
|
+
end
|
320
|
+
end
|
321
|
+
```
|
177
322
|
|
178
|
-
|
323
|
+
#### Execution callbacks
|
179
324
|
|
180
325
|
```ruby
|
181
|
-
NxtPipeline
|
182
|
-
p.
|
183
|
-
|
184
|
-
|
326
|
+
NxtPipeline.new do |p|
|
327
|
+
p.before_execution do |_, change_set|
|
328
|
+
change_set[:acc] << 'before execution 1'
|
329
|
+
change_set
|
330
|
+
end
|
331
|
+
|
332
|
+
p.around_execution do |_, change_set, execution|
|
333
|
+
change_set[:acc] << 'around execution 1'
|
334
|
+
execution.call # you have to specify where in your callback you want to call the inner block
|
335
|
+
change_set[:acc] << 'around execution 1'
|
336
|
+
change_set
|
185
337
|
end
|
186
|
-
|
187
|
-
p.
|
188
|
-
|
338
|
+
|
339
|
+
p.after_execution do |_, change_set|
|
340
|
+
change_set[:acc] << 'after execution 1'
|
341
|
+
change_set
|
189
342
|
end
|
190
343
|
end
|
191
344
|
```
|
192
345
|
|
193
|
-
Note that the `after_execute` callback will not be called
|
346
|
+
Note that the `after_execute` callback will not be called in case a step raises an error.
|
347
|
+
See the previous section (_Error callbacks_) for how to define callbacks that run in case of errors.
|
348
|
+
|
349
|
+
### Constructor resolvers
|
194
350
|
|
195
|
-
|
351
|
+
You can also define constructor resolvers for a pipeline to dynamically define which previously registered constructor
|
352
|
+
to use for a step based on the argument and options passed to the step.
|
196
353
|
|
197
|
-
|
198
|
-
|
199
|
-
|
354
|
+
```ruby
|
355
|
+
class Transform
|
356
|
+
def initialize(word, operation)
|
357
|
+
@word = word
|
358
|
+
@operation = operation
|
359
|
+
end
|
200
360
|
|
361
|
+
attr_reader :word, :operation
|
362
|
+
|
363
|
+
def call
|
364
|
+
word.send(operation)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
NxtPipeline.new do |pipeline|
|
369
|
+
# dynamically resolve to use a proc as constructor
|
370
|
+
pipeline.constructor_resolver do |argument, **opts|
|
371
|
+
argument.is_a?(Class) &&
|
372
|
+
->(step, arg:) {
|
373
|
+
result = step.argument.new(arg, opts.fetch(:operation)).call
|
374
|
+
# OR result = step.argument.new(arg, step.operation).call
|
375
|
+
{ arg: result }
|
376
|
+
}
|
377
|
+
end
|
378
|
+
|
379
|
+
# dynamically resolve to a defined constructor
|
380
|
+
pipeline.constructor_resolver do |argument|
|
381
|
+
argument.is_a?(String) && :dynamic
|
382
|
+
end
|
383
|
+
|
384
|
+
pipeline.constructor(:dynamic) do |step, arg:|
|
385
|
+
if step.argument == 'multiply'
|
386
|
+
{ arg: arg * step.multiplier }
|
387
|
+
elsif step.argument == 'symbolize'
|
388
|
+
{ arg: arg.to_sym }
|
389
|
+
else
|
390
|
+
raise ArgumentError, "Don't know how to deal with argument: #{step.argument}"
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
pipeline.step Transform, operation: 'upcase'
|
395
|
+
pipeline.step 'multiply', multiplier: 2
|
396
|
+
pipeline.step 'symbolize'
|
397
|
+
pipeline.step :extract_value do |arg|
|
398
|
+
arg
|
399
|
+
end
|
400
|
+
end
|
401
|
+
```
|
402
|
+
|
403
|
+
### Configurations
|
404
|
+
|
405
|
+
You probably do not have that many different kinds of steps that you execute within your pipelines. Otherwise the whole
|
406
|
+
concept does not make much sense. To make constructing a pipeline simpler you can therefore define configurations on
|
407
|
+
a global level simply by providing a name for a configuration along with a configuration block.
|
408
|
+
Then you then create a preconfigure pipeline by passing in the name of the configuration when creating a new pipeline.
|
409
|
+
|
410
|
+
```ruby
|
411
|
+
# Define configurations in your initializer or somewhere upfront
|
412
|
+
NxtPipeline.configuration(:test_processor) do |pipeline|
|
413
|
+
pipeline.constructor(:processor) do |arg, step|
|
414
|
+
{ arg: step.argument.call(arg: arg) }
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
NxtPipeline.configure(:validator) do |pipeline|
|
419
|
+
pipeline.constructor(:validator) do |arg, step|
|
420
|
+
# ..
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
# ...
|
425
|
+
|
426
|
+
# Later create a pipeline with a previously defined configuration
|
427
|
+
NxtPipeline.new(configuration: :test_processor) do |p|
|
428
|
+
p.step ->(arg) { arg + 'first ' }, constructor: :processor
|
429
|
+
p.step ->(arg) { arg + 'second ' }, constructor: :processor
|
430
|
+
p.step ->(arg) { arg + 'third' }, constructor: :processor
|
431
|
+
end
|
432
|
+
```
|
433
|
+
|
434
|
+
### Step status and meta_data
|
435
|
+
When executing your steps you can also log the status of a step by setting it in your constructors or callbacks in
|
436
|
+
which you have access to the steps.
|
437
|
+
|
438
|
+
```ruby
|
439
|
+
pipeline = NxtPipeline.new do |pipeline|
|
440
|
+
pipeline.constructor(:step, default: true) do |acc, step|
|
441
|
+
result = step.proc.call(acc)
|
442
|
+
step.status = result.present? # Set the status here
|
443
|
+
step.meta_data = 'additional info' # or some meta data
|
444
|
+
acc
|
445
|
+
end
|
446
|
+
|
447
|
+
pipeline.step :first_step do |acc, step|
|
448
|
+
step.status = 'it worked'
|
449
|
+
step.meta_data = { extra: 'info' }
|
450
|
+
acc
|
451
|
+
end
|
452
|
+
|
453
|
+
pipeline.step :second, proc: ->(acc) { acc }
|
454
|
+
end
|
455
|
+
|
456
|
+
pipeline.logger.log # => { "first_step" => 'it worked', "second" => true }
|
457
|
+
pipeline.steps.map(&:meta_data) # => [{:extra=>"info"}, "additional info"]
|
458
|
+
```
|
201
459
|
|
202
460
|
## Topics
|
203
|
-
- Step orchestration (chainable steps)
|
204
|
-
- Constructors should take arg as first and step as second arg
|
205
461
|
|
206
462
|
## Development
|
207
463
|
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module NxtPipeline
|
2
|
+
class Callbacks
|
3
|
+
def initialize(pipeline:)
|
4
|
+
@registry = build_registry
|
5
|
+
@pipeline = pipeline
|
6
|
+
end
|
7
|
+
|
8
|
+
def register(path, callback)
|
9
|
+
registry.resolve!(*path) << callback
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(kind_of_callback, type, change_set)
|
13
|
+
callbacks = registry.resolve!(kind_of_callback, type)
|
14
|
+
return unless callbacks.any?
|
15
|
+
|
16
|
+
callbacks.inject(change_set) do |changes, callback|
|
17
|
+
run_callback(callback, changes)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def around(type, change_set, &execution)
|
22
|
+
around_callbacks = registry.resolve!(:around, type)
|
23
|
+
return execution.call unless around_callbacks.any?
|
24
|
+
|
25
|
+
callback_chain = around_callbacks.reverse.inject(execution) do |previous, callback|
|
26
|
+
-> { callback.call(change_set, previous, pipeline) }
|
27
|
+
end
|
28
|
+
|
29
|
+
callback_chain.call
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :registry, :pipeline
|
35
|
+
|
36
|
+
def run_callback(callback, change_set)
|
37
|
+
args = [change_set, pipeline]
|
38
|
+
args = args.take(callback.arity) unless callback.arity.negative?
|
39
|
+
callback.call(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_registry
|
43
|
+
NxtRegistry::Registry.new(:callbacks) do
|
44
|
+
register(:before) do
|
45
|
+
register(:step, [])
|
46
|
+
register(:execution, [])
|
47
|
+
end
|
48
|
+
|
49
|
+
register(:around) do
|
50
|
+
register(:step, [])
|
51
|
+
register(:execution, [])
|
52
|
+
end
|
53
|
+
|
54
|
+
register(:after) do
|
55
|
+
register(:step, [])
|
56
|
+
register(:execution, [])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -20,8 +20,11 @@ module NxtPipeline
|
|
20
20
|
(error.class.ancestors & errors).any?
|
21
21
|
end
|
22
22
|
|
23
|
-
def call(
|
24
|
-
|
23
|
+
def call(error, acc, step)
|
24
|
+
args = [error, acc, step]
|
25
|
+
args = args.take(callback.arity) unless callback.arity.negative?
|
26
|
+
|
27
|
+
callback.call(*args)
|
25
28
|
end
|
26
29
|
end
|
27
30
|
end
|