nxt_pipeline 0.4.2 → 2.0.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.
- 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
|