nxt_pipeline 1.0.0 → 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 +11 -0
- data/Gemfile.lock +27 -29
- data/README.md +312 -93
- data/lib/nxt_pipeline/callbacks.rb +8 -5
- data/lib/nxt_pipeline/error_callback.rb +5 -2
- data/lib/nxt_pipeline/pipeline.rb +128 -56
- data/lib/nxt_pipeline/step.rb +45 -28
- data/lib/nxt_pipeline/version.rb +1 -1
- data/lib/nxt_pipeline.rb +38 -1
- metadata +6 -7
- data/lib/nxt_pipeline/constructor.rb +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5682c4e905d939b057c79b775c8401f1da648a0e5793c1c438dfef3cdb741147
|
|
4
|
+
data.tar.gz: 8908bf0ab622f472ccdb1a91cce704b0e88181352f4dfa0bb1942a684b60729b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e33df278d8ba33998cc798f6b1a97461360e81af3a0bad83549d74bf00190e8ef7d4aa2459f513bc1e4b3e750785058ae708c90136bfc17bebf0faf79fa7aed0
|
|
7
|
+
data.tar.gz: '09d1caf2cd41693f56fddbcf6e70e4ba4b2212b7ce9fd3669e736a6afa39245a177d79171a5819bd3842192a5a3927931a4cb99dd86a3b98e24a834e18460218'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## nxt_pipeline 2.0.0 (10.05.2022)
|
|
2
|
+
|
|
3
|
+
- Rename `Pipeline.execute` to `Pipeline.call`
|
|
4
|
+
- Pipeline#call accepts any argument instead of just key word arguments or hashes
|
|
5
|
+
- Introduce constructor resolvers
|
|
6
|
+
- Expose :new and :call directly on NxtPipeline instead of only through NxtPipeline::Pipeline class
|
|
7
|
+
- Change step DSL: Introduce constructor option to specify the constructor to use for a step
|
|
8
|
+
- Introduce Configurations
|
|
9
|
+
- Expose step.status and step.meta_data accessors to set status and meta_data of steps in constructors
|
|
10
|
+
- Change arguments of error callbacks to be error, acc, step instead of acc, step, error and only pass by arity of callback
|
|
11
|
+
|
|
1
12
|
## nxt_pipeline 1.0.0 (24.11.2020)
|
|
2
13
|
|
|
3
14
|
Replace after and before execute hooks with proper callbacks.
|
data/Gemfile.lock
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
nxt_pipeline (
|
|
4
|
+
nxt_pipeline (2.0.0)
|
|
5
5
|
activesupport
|
|
6
6
|
nxt_registry
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
-
activesupport (
|
|
11
|
+
activesupport (7.0.3)
|
|
12
12
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
13
13
|
i18n (>= 1.6, < 2)
|
|
14
14
|
minitest (>= 5.1)
|
|
15
15
|
tzinfo (~> 2.0)
|
|
16
|
-
zeitwerk (~> 2.3)
|
|
17
16
|
byebug (11.1.3)
|
|
18
17
|
coderay (1.1.3)
|
|
19
|
-
concurrent-ruby (1.1.
|
|
20
|
-
diff-lcs (1.
|
|
21
|
-
ffi (1.
|
|
22
|
-
formatador (
|
|
23
|
-
guard (2.
|
|
18
|
+
concurrent-ruby (1.1.10)
|
|
19
|
+
diff-lcs (1.5.0)
|
|
20
|
+
ffi (1.15.5)
|
|
21
|
+
formatador (1.1.0)
|
|
22
|
+
guard (2.18.0)
|
|
24
23
|
formatador (>= 0.2.4)
|
|
25
24
|
listen (>= 2.7, < 4.0)
|
|
26
25
|
lumberjack (>= 1.0.12, < 2.0)
|
|
27
26
|
nenv (~> 0.1)
|
|
28
27
|
notiffany (~> 0.0)
|
|
29
|
-
pry (>= 0.
|
|
28
|
+
pry (>= 0.13.0)
|
|
30
29
|
shellany (~> 0.0)
|
|
31
30
|
thor (>= 0.18.1)
|
|
32
31
|
guard-compat (1.2.1)
|
|
@@ -34,19 +33,19 @@ GEM
|
|
|
34
33
|
guard (~> 2.1)
|
|
35
34
|
guard-compat (~> 1.1)
|
|
36
35
|
rspec (>= 2.99.0, < 4.0)
|
|
37
|
-
i18n (1.
|
|
36
|
+
i18n (1.10.0)
|
|
38
37
|
concurrent-ruby (~> 1.0)
|
|
39
|
-
listen (3.
|
|
38
|
+
listen (3.7.1)
|
|
40
39
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
41
40
|
rb-inotify (~> 0.9, >= 0.9.10)
|
|
42
41
|
lumberjack (1.2.8)
|
|
43
42
|
method_source (1.0.0)
|
|
44
|
-
minitest (5.
|
|
43
|
+
minitest (5.15.0)
|
|
45
44
|
nenv (0.3.0)
|
|
46
45
|
notiffany (0.1.3)
|
|
47
46
|
nenv (~> 0.1)
|
|
48
47
|
shellany (~> 0.0)
|
|
49
|
-
nxt_registry (0.3.
|
|
48
|
+
nxt_registry (0.3.10)
|
|
50
49
|
activesupport
|
|
51
50
|
pry (0.13.1)
|
|
52
51
|
coderay (~> 1.1)
|
|
@@ -54,30 +53,29 @@ GEM
|
|
|
54
53
|
pry-byebug (3.9.0)
|
|
55
54
|
byebug (~> 11.0)
|
|
56
55
|
pry (~> 0.13.0)
|
|
57
|
-
rake (13.0.
|
|
58
|
-
rb-fsevent (0.
|
|
56
|
+
rake (13.0.6)
|
|
57
|
+
rb-fsevent (0.11.1)
|
|
59
58
|
rb-inotify (0.10.1)
|
|
60
59
|
ffi (~> 1.0)
|
|
61
|
-
rspec (3.
|
|
62
|
-
rspec-core (~> 3.
|
|
63
|
-
rspec-expectations (~> 3.
|
|
64
|
-
rspec-mocks (~> 3.
|
|
65
|
-
rspec-core (3.
|
|
66
|
-
rspec-support (~> 3.
|
|
67
|
-
rspec-expectations (3.
|
|
60
|
+
rspec (3.11.0)
|
|
61
|
+
rspec-core (~> 3.11.0)
|
|
62
|
+
rspec-expectations (~> 3.11.0)
|
|
63
|
+
rspec-mocks (~> 3.11.0)
|
|
64
|
+
rspec-core (3.11.0)
|
|
65
|
+
rspec-support (~> 3.11.0)
|
|
66
|
+
rspec-expectations (3.11.0)
|
|
68
67
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
69
|
-
rspec-support (~> 3.
|
|
70
|
-
rspec-mocks (3.
|
|
68
|
+
rspec-support (~> 3.11.0)
|
|
69
|
+
rspec-mocks (3.11.1)
|
|
71
70
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
72
|
-
rspec-support (~> 3.
|
|
73
|
-
rspec-support (3.
|
|
74
|
-
rspec_junit_formatter (0.
|
|
71
|
+
rspec-support (~> 3.11.0)
|
|
72
|
+
rspec-support (3.11.0)
|
|
73
|
+
rspec_junit_formatter (0.5.1)
|
|
75
74
|
rspec-core (>= 2, < 4, != 2.12.0)
|
|
76
75
|
shellany (0.0.1)
|
|
77
|
-
thor (1.1
|
|
76
|
+
thor (1.2.1)
|
|
78
77
|
tzinfo (2.0.4)
|
|
79
78
|
concurrent-ruby (~> 1.0)
|
|
80
|
-
zeitwerk (2.4.2)
|
|
81
79
|
|
|
82
80
|
PLATFORMS
|
|
83
81
|
ruby
|
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,155 +32,260 @@ 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
|
|
41
50
|
end
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
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}"
|
|
45
57
|
end
|
|
46
58
|
end
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
52
72
|
end
|
|
53
73
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
class MaxSize < Validator
|
|
75
|
+
def initialize(value, size:)
|
|
76
|
+
@value = value
|
|
77
|
+
@size = size
|
|
78
|
+
end
|
|
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}"
|
|
85
|
+
end
|
|
61
86
|
end
|
|
87
|
+
|
|
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
|
|
100
|
+
end
|
|
101
|
+
|
|
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'] }
|
|
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.
|
|
124
|
+
|
|
125
|
+
Make a constructor available for all pipelines of your project by defining it globally with:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
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?
|
|
132
|
+
|
|
133
|
+
acc
|
|
134
|
+
end
|
|
135
|
+
```
|
|
65
136
|
|
|
66
|
-
|
|
137
|
+
Or define a constructor only locally for a specific pipeline.
|
|
67
138
|
|
|
68
139
|
```ruby
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
74
148
|
|
|
75
|
-
|
|
149
|
+
p.step TypeChecker, options: { type: String }
|
|
76
150
|
# ...
|
|
77
151
|
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Constructor Hierarchy
|
|
78
155
|
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
NxtPipeline.new({}) do |p|
|
|
165
|
+
p.constructor(:service) do |acc, step|
|
|
166
|
+
step.service_class.new(acc).call
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
p.step :service, service_class: MyServiceClass
|
|
170
|
+
p.step :service, service_class: MyOtherServiceClass
|
|
171
|
+
# ...
|
|
81
172
|
end
|
|
82
173
|
```
|
|
83
174
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
will be set to :inline.
|
|
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.
|
|
87
177
|
|
|
88
|
-
###
|
|
178
|
+
### Defining steps
|
|
89
179
|
|
|
90
|
-
|
|
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.
|
|
91
184
|
|
|
92
185
|
```ruby
|
|
93
|
-
|
|
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
|
+
# ...
|
|
197
|
+
end
|
|
198
|
+
```
|
|
94
199
|
|
|
95
|
-
|
|
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.
|
|
96
202
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Execution
|
|
103
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.
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
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
|
|
232
|
+
end
|
|
104
233
|
```
|
|
105
234
|
|
|
106
|
-
You can also
|
|
235
|
+
You can also create a new instance of a pipeline and directly run it with `call`:
|
|
107
236
|
|
|
108
237
|
```ruby
|
|
109
|
-
NxtPipeline
|
|
110
|
-
p.
|
|
111
|
-
{ arg: arg.upcase }
|
|
112
|
-
end
|
|
238
|
+
NxtPipeline.call(arg: 'initial argument') do |p|
|
|
239
|
+
p.steps # ...
|
|
113
240
|
end
|
|
114
241
|
```
|
|
115
242
|
|
|
116
243
|
You can query the steps of your pipeline simply by calling `pipeline.steps`. A NxtPipeline::Step will provide you with
|
|
117
|
-
an interface
|
|
244
|
+
an interface for options, status, execution_finished_at execution_started_at,
|
|
245
|
+
execution_duration, result, error and the index in the pipeline.
|
|
118
246
|
|
|
119
247
|
```
|
|
120
248
|
pipeline.steps.first
|
|
121
|
-
# will give you
|
|
122
|
-
|
|
123
|
-
#<NxtPipeline::Step:0x00007f83eb399448
|
|
124
|
-
@constructor=
|
|
125
|
-
#<Proc:0x00007f83eb399498@/Users/andy/workspace/nxt_pipeline/spec/pipeline_spec.rb:467>,
|
|
126
|
-
@error=nil,
|
|
127
|
-
@index=0,
|
|
128
|
-
@opts={:to_s=>:transformer, :method=>:upcase},
|
|
129
|
-
@result=nil,
|
|
130
|
-
@status=nil,
|
|
131
|
-
@type=:transformer
|
|
132
|
-
@execution_duration=1.0e-05,
|
|
133
|
-
@execution_finished_at=2020-10-22 15:52:55.806417 +0100,
|
|
134
|
-
@execution_started_at=2020-10-22 15:52:55.806407 +0100,>
|
|
249
|
+
# will give you a step object
|
|
250
|
+
#<NxtPipeline::Step:0x00007f83eb399448...>
|
|
135
251
|
```
|
|
136
252
|
|
|
137
253
|
### Guard clauses
|
|
138
254
|
|
|
139
255
|
You can also define guard clauses that take a proc to prevent the execution of a step.
|
|
140
|
-
|
|
256
|
+
A guard can accept the change set and the step as arguments.
|
|
141
257
|
|
|
142
258
|
```ruby
|
|
143
|
-
pipeline.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
147
263
|
|
|
148
264
|
```
|
|
149
265
|
|
|
150
266
|
### Error callbacks
|
|
151
267
|
|
|
152
|
-
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`.
|
|
153
270
|
|
|
154
271
|
```ruby
|
|
155
|
-
NxtPipeline
|
|
156
|
-
p.step
|
|
157
|
-
{ arg: arg.upcase }
|
|
158
|
-
end
|
|
272
|
+
NxtPipeline.new do |p|
|
|
273
|
+
p.step # ...
|
|
159
274
|
|
|
160
|
-
p.on_error MyCustomError do |
|
|
275
|
+
p.on_error MyCustomError do |error|
|
|
161
276
|
# First matching error callback will be executed!
|
|
162
277
|
end
|
|
163
278
|
|
|
164
|
-
p.on_errors ArgumentError, KeyError do |
|
|
279
|
+
p.on_errors ArgumentError, KeyError do |error, acc|
|
|
165
280
|
# First matching error callback will be executed!
|
|
166
281
|
end
|
|
167
282
|
|
|
168
|
-
p.on_errors YetAnotherError, halt_on_error: false do |
|
|
283
|
+
p.on_errors YetAnotherError, halt_on_error: false do |error, acc, step|
|
|
169
284
|
# After executing the callback the pipeline will not halt but continue to
|
|
170
285
|
# execute the next steps.
|
|
171
286
|
end
|
|
172
287
|
|
|
173
|
-
p.on_errors do |
|
|
288
|
+
p.on_errors do |error, acc, step|
|
|
174
289
|
# This will match all errors inheriting from StandardError
|
|
175
290
|
end
|
|
176
291
|
end
|
|
@@ -179,12 +294,13 @@ end
|
|
|
179
294
|
### Before, around and after callbacks
|
|
180
295
|
|
|
181
296
|
You can also define callbacks :before, :around and :after each step and or the `#execute` method. You can also register
|
|
182
|
-
multiple callbacks, but probably you want to keep them to a minimum to not end up in hell.
|
|
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.
|
|
183
299
|
|
|
184
300
|
#### Step callbacks
|
|
185
301
|
|
|
186
302
|
```ruby
|
|
187
|
-
NxtPipeline
|
|
303
|
+
NxtPipeline.new do |p|
|
|
188
304
|
p.before_step do |_, change_set|
|
|
189
305
|
change_set[:acc] << 'before step 1'
|
|
190
306
|
change_set
|
|
@@ -207,7 +323,7 @@ end
|
|
|
207
323
|
#### Execution callbacks
|
|
208
324
|
|
|
209
325
|
```ruby
|
|
210
|
-
NxtPipeline
|
|
326
|
+
NxtPipeline.new do |p|
|
|
211
327
|
p.before_execution do |_, change_set|
|
|
212
328
|
change_set[:acc] << 'before execution 1'
|
|
213
329
|
change_set
|
|
@@ -227,18 +343,121 @@ NxtPipeline::Pipeline.new do |p|
|
|
|
227
343
|
end
|
|
228
344
|
```
|
|
229
345
|
|
|
230
|
-
Note that the `after_execute` callback will not be called in case a step raises an error.
|
|
346
|
+
Note that the `after_execute` callback will not be called in case a step raises an error.
|
|
231
347
|
See the previous section (_Error callbacks_) for how to define callbacks that run in case of errors.
|
|
232
348
|
|
|
233
|
-
###
|
|
349
|
+
### Constructor resolvers
|
|
350
|
+
|
|
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.
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
class Transform
|
|
356
|
+
def initialize(word, operation)
|
|
357
|
+
@word = word
|
|
358
|
+
@operation = operation
|
|
359
|
+
end
|
|
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
|
|
234
423
|
|
|
235
|
-
|
|
236
|
-
You can also use this if you are not fine with resolving the constructor from the step argument. Check out the
|
|
237
|
-
`nxt_pipeline/spec/step_resolver_spec.rb` for examples how you can implement your own step_resolvers.
|
|
424
|
+
# ...
|
|
238
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
|
+
```
|
|
239
459
|
|
|
240
460
|
## Topics
|
|
241
|
-
- Constructors should take arg as first and step as second arg
|
|
242
461
|
|
|
243
462
|
## Development
|
|
244
463
|
|
|
@@ -10,8 +10,11 @@ module NxtPipeline
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def run(kind_of_callback, type, change_set)
|
|
13
|
-
registry.resolve!(kind_of_callback, type)
|
|
14
|
-
|
|
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)
|
|
15
18
|
end
|
|
16
19
|
end
|
|
17
20
|
|
|
@@ -20,7 +23,7 @@ module NxtPipeline
|
|
|
20
23
|
return execution.call unless around_callbacks.any?
|
|
21
24
|
|
|
22
25
|
callback_chain = around_callbacks.reverse.inject(execution) do |previous, callback|
|
|
23
|
-
-> { callback.call(
|
|
26
|
+
-> { callback.call(change_set, previous, pipeline) }
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
callback_chain.call
|
|
@@ -31,8 +34,8 @@ module NxtPipeline
|
|
|
31
34
|
attr_reader :registry, :pipeline
|
|
32
35
|
|
|
33
36
|
def run_callback(callback, change_set)
|
|
34
|
-
args = [
|
|
35
|
-
args = args.take(callback.arity)
|
|
37
|
+
args = [change_set, pipeline]
|
|
38
|
+
args = args.take(callback.arity) unless callback.arity.negative?
|
|
36
39
|
callback.call(*args)
|
|
37
40
|
end
|
|
38
41
|
|
|
@@ -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
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
module NxtPipeline
|
|
2
2
|
class Pipeline
|
|
3
|
-
def self.
|
|
4
|
-
new(&block).
|
|
3
|
+
def self.call(acc, configuration: nil, resolvers: [], &block)
|
|
4
|
+
new(configuration: configuration, resolvers: resolvers, &block).call(acc)
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
-
def initialize(
|
|
7
|
+
def initialize(configuration: nil, resolvers: [], &block)
|
|
8
8
|
@steps = []
|
|
9
9
|
@error_callbacks = []
|
|
10
10
|
@logger = Logger.new
|
|
@@ -12,27 +12,34 @@ module NxtPipeline
|
|
|
12
12
|
@current_arg = nil
|
|
13
13
|
@default_constructor_name = nil
|
|
14
14
|
@constructors = {}
|
|
15
|
-
@
|
|
15
|
+
@constructor_resolvers = resolvers
|
|
16
|
+
@result = nil
|
|
17
|
+
|
|
18
|
+
if configuration.present?
|
|
19
|
+
config = ::NxtPipeline.configuration(configuration)
|
|
20
|
+
configure(&config)
|
|
21
|
+
end
|
|
16
22
|
|
|
17
23
|
configure(&block) if block_given?
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
alias_method :configure, :tap
|
|
21
27
|
|
|
22
|
-
attr_accessor :logger
|
|
28
|
+
attr_accessor :logger
|
|
29
|
+
attr_reader :result
|
|
23
30
|
|
|
24
|
-
def constructor(name,
|
|
31
|
+
def constructor(name, default: false, &constructor)
|
|
25
32
|
name = name.to_sym
|
|
26
33
|
raise StandardError, "Already registered step :#{name}" if constructors[name]
|
|
27
34
|
|
|
28
|
-
constructors[name] =
|
|
35
|
+
constructors[name] = constructor
|
|
29
36
|
|
|
30
|
-
return unless
|
|
37
|
+
return unless default
|
|
31
38
|
set_default_constructor(name)
|
|
32
39
|
end
|
|
33
40
|
|
|
34
|
-
def
|
|
35
|
-
|
|
41
|
+
def constructor_resolver(&block)
|
|
42
|
+
constructor_resolvers << block
|
|
36
43
|
end
|
|
37
44
|
|
|
38
45
|
def set_default_constructor(default_constructor)
|
|
@@ -44,61 +51,128 @@ module NxtPipeline
|
|
|
44
51
|
raise ArgumentError, 'Default step already defined'
|
|
45
52
|
end
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
# Overwrite reader to also define steps
|
|
55
|
+
def steps(steps = [])
|
|
56
|
+
return @steps unless steps.any?
|
|
57
|
+
|
|
58
|
+
steps.each do |args|
|
|
59
|
+
arguments = Array(args)
|
|
60
|
+
if arguments.size == 1
|
|
61
|
+
step(arguments.first)
|
|
62
|
+
elsif arguments.size == 2
|
|
63
|
+
step(arguments.first, **arguments.second)
|
|
64
|
+
else
|
|
65
|
+
raise ArgumentError, "Either pass a single argument or an argument and options"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Allow to force steps with setter
|
|
71
|
+
def steps=(steps = [])
|
|
72
|
+
# reset steps to be zero
|
|
73
|
+
@steps = []
|
|
74
|
+
steps(steps)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def step(argument, constructor: nil, **opts, &block)
|
|
78
|
+
|
|
79
|
+
if constructor.present? and block_given?
|
|
80
|
+
msg = "Either specify a block or a constructor but not both at the same time!"
|
|
81
|
+
raise ArgumentError, msg
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
to_s = if opts[:to_s].present?
|
|
85
|
+
opts[:to_s] = opts[:to_s].to_s
|
|
86
|
+
else
|
|
87
|
+
if argument.is_a?(Proc) || argument.is_a?(Method)
|
|
88
|
+
steps.count.to_s
|
|
89
|
+
else
|
|
90
|
+
argument.to_s
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
opts.reverse_merge!(to_s: to_s)
|
|
96
|
+
|
|
97
|
+
if constructor.present?
|
|
98
|
+
# p.step Service, constructor: ->(step, **changes) { ... }
|
|
99
|
+
if constructor.respond_to?(:call)
|
|
100
|
+
resolved_constructor = constructor
|
|
101
|
+
else
|
|
102
|
+
# p.step Service, constructor: :service
|
|
103
|
+
resolved_constructor = constructors.fetch(constructor) {
|
|
104
|
+
::NxtPipeline.constructor(constructor) || (raise ArgumentError, "No constructor defined for #{constructor}")
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
elsif block_given?
|
|
108
|
+
# p.step :inline do ... end
|
|
109
|
+
resolved_constructor = block
|
|
53
110
|
else
|
|
54
|
-
constructor
|
|
55
|
-
|
|
111
|
+
# If no constructor was given try to resolve one
|
|
112
|
+
resolvers = constructor_resolvers.any? ? constructor_resolvers : []
|
|
113
|
+
|
|
114
|
+
constructor_from_resolvers = resolvers.map do |resolver|
|
|
115
|
+
resolver.call(argument, **opts)
|
|
56
116
|
end.find(&:itself)
|
|
57
117
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
118
|
+
# resolved constructor is a proc
|
|
119
|
+
if constructor_from_resolvers.is_a?(Proc)
|
|
120
|
+
resolved_constructor = constructor_from_resolvers
|
|
121
|
+
elsif constructor_from_resolvers.present?
|
|
122
|
+
resolved_constructor = constructors[constructor_from_resolvers]
|
|
63
123
|
else
|
|
64
|
-
|
|
124
|
+
# try to resolve constructor by argument --> #TODO: Is this a good idea?
|
|
125
|
+
resolved_constructor = constructors[argument]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# if still no constructor resolved
|
|
130
|
+
unless resolved_constructor.present?
|
|
131
|
+
if argument.is_a?(NxtPipeline::Pipeline)
|
|
132
|
+
pipeline_constructor = ->(changes) { argument.call(changes) }
|
|
133
|
+
resolved_constructor = pipeline_constructor
|
|
134
|
+
# last chance: default constructor
|
|
135
|
+
elsif default_constructor.present?
|
|
136
|
+
resolved_constructor = default_constructor
|
|
137
|
+
# now we really give up :-(
|
|
138
|
+
else
|
|
139
|
+
raise ArgumentError, "Could neither find nor resolve any constructor for #{argument}, #{opts}"
|
|
140
|
+
end
|
|
65
141
|
end
|
|
66
142
|
end
|
|
67
143
|
|
|
68
|
-
register_step(argument,
|
|
144
|
+
register_step(argument, resolved_constructor, callbacks, **opts)
|
|
69
145
|
end
|
|
70
146
|
|
|
71
|
-
def
|
|
147
|
+
def call(acc, &block)
|
|
72
148
|
reset
|
|
73
149
|
|
|
74
150
|
configure(&block) if block_given?
|
|
75
|
-
callbacks.run(:before, :execution,
|
|
151
|
+
callbacks.run(:before, :execution, acc)
|
|
76
152
|
|
|
77
|
-
result = callbacks.around :execution,
|
|
78
|
-
steps.inject(
|
|
79
|
-
|
|
153
|
+
self.result = callbacks.around :execution, acc do
|
|
154
|
+
steps.inject(acc) do |changes, step|
|
|
155
|
+
self.result = call_step(step, changes)
|
|
80
156
|
rescue StandardError => error
|
|
81
|
-
decorate_error_with_details(error,
|
|
82
|
-
handle_error_of_step(error)
|
|
83
|
-
|
|
157
|
+
decorate_error_with_details(error, changes, step, logger)
|
|
158
|
+
handle_error_of_step(step, error)
|
|
159
|
+
result
|
|
84
160
|
end
|
|
85
161
|
end
|
|
86
162
|
|
|
87
|
-
callbacks.run(:after, :execution,
|
|
88
|
-
result
|
|
163
|
+
# callbacks.run(:after, :execution, result) TODO: Better pass result to callback?
|
|
164
|
+
self.result = callbacks.run(:after, :execution, acc) || result
|
|
89
165
|
rescue StandardError => error
|
|
90
166
|
handle_step_error(error)
|
|
91
167
|
end
|
|
92
168
|
|
|
93
|
-
alias_method :call, :execute
|
|
94
|
-
|
|
95
169
|
def handle_step_error(error)
|
|
96
170
|
log_step(current_step)
|
|
97
171
|
callback = find_error_callback(error)
|
|
98
172
|
|
|
99
173
|
raise unless callback
|
|
100
174
|
|
|
101
|
-
callback.call(
|
|
175
|
+
callback.call(error, current_arg, current_step)
|
|
102
176
|
end
|
|
103
177
|
|
|
104
178
|
def on_errors(*errors, halt_on_error: true, &callback)
|
|
@@ -133,14 +207,15 @@ module NxtPipeline
|
|
|
133
207
|
|
|
134
208
|
private
|
|
135
209
|
|
|
210
|
+
attr_writer :result
|
|
211
|
+
|
|
136
212
|
def callbacks
|
|
137
213
|
@callbacks ||= NxtPipeline::Callbacks.new(pipeline: self)
|
|
138
214
|
end
|
|
139
215
|
|
|
140
|
-
attr_reader :error_callbacks, :constructors, :
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
:default_constructor_name
|
|
216
|
+
attr_reader :error_callbacks, :constructors, :constructor_resolvers
|
|
217
|
+
attr_writer :default_constructor_name
|
|
218
|
+
attr_accessor :current_step, :current_arg
|
|
144
219
|
|
|
145
220
|
def default_constructor
|
|
146
221
|
return unless default_constructor_name
|
|
@@ -148,12 +223,12 @@ module NxtPipeline
|
|
|
148
223
|
@default_constructor ||= constructors[default_constructor_name.to_sym]
|
|
149
224
|
end
|
|
150
225
|
|
|
151
|
-
def
|
|
226
|
+
def call_step(step, acc)
|
|
152
227
|
self.current_step = step
|
|
153
|
-
self.current_arg =
|
|
154
|
-
result = step.
|
|
228
|
+
self.current_arg = acc
|
|
229
|
+
result = step.call(acc)
|
|
155
230
|
log_step(step)
|
|
156
|
-
result ||
|
|
231
|
+
result || acc
|
|
157
232
|
end
|
|
158
233
|
|
|
159
234
|
def find_error_callback(error)
|
|
@@ -171,13 +246,6 @@ module NxtPipeline
|
|
|
171
246
|
self.current_step = nil
|
|
172
247
|
end
|
|
173
248
|
|
|
174
|
-
def raise_reserved_type_inline_error
|
|
175
|
-
raise ArgumentError, 'Type :inline is reserved for inline steps!'
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def default_step_resolvers
|
|
179
|
-
[->(step_argument) { step_argument.is_a?(Symbol) && step_argument }]
|
|
180
|
-
end
|
|
181
249
|
|
|
182
250
|
def decorate_error_with_details(error, change_set, step, logger)
|
|
183
251
|
error.define_singleton_method :details do
|
|
@@ -194,14 +262,18 @@ module NxtPipeline
|
|
|
194
262
|
steps << Step.new(argument, constructor, steps.count, self, callbacks, **opts)
|
|
195
263
|
end
|
|
196
264
|
|
|
197
|
-
def handle_error_of_step(error)
|
|
265
|
+
def handle_error_of_step(step, error)
|
|
198
266
|
error_callback = find_error_callback(error)
|
|
199
267
|
raise error unless error_callback.present? && error_callback.continue_after_error?
|
|
200
268
|
|
|
201
|
-
log_step(
|
|
269
|
+
log_step(step)
|
|
202
270
|
raise error unless error_callback.present?
|
|
203
271
|
|
|
204
|
-
error_callback.call(
|
|
272
|
+
error_callback.call(error, current_arg, step)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def default_constructor_name
|
|
276
|
+
@default_constructor_name || ::NxtPipeline.default_constructor_name
|
|
205
277
|
end
|
|
206
278
|
end
|
|
207
279
|
end
|
data/lib/nxt_pipeline/step.rb
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
module NxtPipeline
|
|
2
2
|
class Step
|
|
3
|
+
RESERVED_OPTION_KEYS = %i[to_s unless if]
|
|
4
|
+
|
|
3
5
|
def initialize(argument, constructor, index, pipeline, callbacks, **opts)
|
|
4
|
-
|
|
6
|
+
@opts = opts.symbolize_keys
|
|
5
7
|
|
|
6
8
|
@pipeline = pipeline
|
|
7
9
|
@callbacks = callbacks
|
|
8
10
|
@argument = argument
|
|
9
11
|
@index = index
|
|
10
|
-
@opts = opts
|
|
11
12
|
@constructor = constructor
|
|
12
|
-
@to_s =
|
|
13
|
+
@to_s = opts.fetch(:to_s) { argument }
|
|
13
14
|
@options_mapper = opts[:map_options]
|
|
14
15
|
|
|
15
16
|
@status = nil
|
|
16
17
|
@result = nil
|
|
17
18
|
@error = nil
|
|
18
19
|
@mapped_options = nil
|
|
20
|
+
@meta_data = nil
|
|
21
|
+
|
|
22
|
+
define_option_readers
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
attr_reader :argument,
|
|
22
26
|
:result,
|
|
23
|
-
:status,
|
|
24
27
|
:execution_started_at,
|
|
25
28
|
:execution_finished_at,
|
|
26
29
|
:execution_duration,
|
|
@@ -29,25 +32,23 @@ module NxtPipeline
|
|
|
29
32
|
:index,
|
|
30
33
|
:mapped_options
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
attr_writer :to_s
|
|
36
|
+
attr_accessor :meta_data, :status
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
alias_method :name, :to_s
|
|
36
|
-
|
|
37
|
-
def execute(**change_set)
|
|
38
|
+
def call(acc)
|
|
38
39
|
track_execution_time do
|
|
39
|
-
set_mapped_options(
|
|
40
|
-
guard_args = [
|
|
40
|
+
set_mapped_options(acc)
|
|
41
|
+
guard_args = [acc, self]
|
|
41
42
|
|
|
42
|
-
callbacks.run(:before, :step,
|
|
43
|
+
callbacks.run(:before, :step, acc)
|
|
43
44
|
|
|
44
45
|
if evaluate_unless_guard(guard_args) && evaluate_if_guard(guard_args)
|
|
45
|
-
callbacks.around(:step,
|
|
46
|
-
set_result(
|
|
46
|
+
callbacks.around(:step, acc) do
|
|
47
|
+
set_result(acc)
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
callbacks.run(:after, :step,
|
|
51
|
+
callbacks.run(:after, :step, acc)
|
|
51
52
|
|
|
52
53
|
set_status
|
|
53
54
|
result
|
|
@@ -58,15 +59,19 @@ module NxtPipeline
|
|
|
58
59
|
raise
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
def set_mapped_options(
|
|
62
|
+
def set_mapped_options(acc)
|
|
62
63
|
mapper = options_mapper || default_options_mapper
|
|
63
|
-
mapper_args = [
|
|
64
|
+
mapper_args = [acc, self].take(mapper.arity)
|
|
64
65
|
self.mapped_options = mapper.call(*mapper_args)
|
|
65
66
|
end
|
|
66
67
|
|
|
68
|
+
def to_s
|
|
69
|
+
@to_s.to_s
|
|
70
|
+
end
|
|
71
|
+
|
|
67
72
|
private
|
|
68
73
|
|
|
69
|
-
attr_writer :result, :
|
|
74
|
+
attr_writer :result, :error, :mapped_options, :execution_started_at, :execution_finished_at, :execution_duration
|
|
70
75
|
attr_reader :constructor, :options_mapper, :pipeline, :callbacks
|
|
71
76
|
|
|
72
77
|
def evaluate_if_guard(args)
|
|
@@ -77,19 +82,15 @@ module NxtPipeline
|
|
|
77
82
|
!execute_callable(unless_guard, args)
|
|
78
83
|
end
|
|
79
84
|
|
|
80
|
-
def set_result(
|
|
81
|
-
args = [
|
|
85
|
+
def set_result(acc)
|
|
86
|
+
args = [acc, self]
|
|
82
87
|
self.result = execute_callable(constructor, args)
|
|
83
88
|
end
|
|
84
89
|
|
|
85
90
|
def execute_callable(callable, args)
|
|
86
|
-
args =
|
|
91
|
+
args = args.take(callable.arity) unless callable.arity.negative?
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
callable.call(*args.take(args.length - 1), **args.last)
|
|
90
|
-
else
|
|
91
|
-
callable.call(*args)
|
|
92
|
-
end
|
|
93
|
+
callable.call(*args)
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
def if_guard
|
|
@@ -104,8 +105,10 @@ module NxtPipeline
|
|
|
104
105
|
-> { result }
|
|
105
106
|
end
|
|
106
107
|
|
|
107
|
-
def
|
|
108
|
-
|
|
108
|
+
def define_option_readers
|
|
109
|
+
raise ArgumentError, "#{invalid_option_keys} are not allowed as options" if invalid_option_keys.any?
|
|
110
|
+
|
|
111
|
+
options_without_reserved_options.each do |key, value|
|
|
109
112
|
define_singleton_method key.to_s do
|
|
110
113
|
value
|
|
111
114
|
end
|
|
@@ -113,6 +116,8 @@ module NxtPipeline
|
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
def set_status
|
|
119
|
+
return if status.present? # We do not set it if the constructor did already
|
|
120
|
+
|
|
116
121
|
self.status = result.present? ? :success : :skipped
|
|
117
122
|
end
|
|
118
123
|
|
|
@@ -140,5 +145,17 @@ module NxtPipeline
|
|
|
140
145
|
# returns an empty hash
|
|
141
146
|
->(_) { {} }
|
|
142
147
|
end
|
|
148
|
+
|
|
149
|
+
def options_without_reserved_options
|
|
150
|
+
opts.except(*reserved_option_keys)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def reserved_option_keys
|
|
154
|
+
@reserved_option_keys ||= methods + RESERVED_OPTION_KEYS
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def invalid_option_keys
|
|
158
|
+
opts.except(*RESERVED_OPTION_KEYS).keys & methods
|
|
159
|
+
end
|
|
143
160
|
end
|
|
144
161
|
end
|
data/lib/nxt_pipeline/version.rb
CHANGED
data/lib/nxt_pipeline.rb
CHANGED
|
@@ -2,11 +2,48 @@ require 'active_support/all'
|
|
|
2
2
|
require 'nxt_registry'
|
|
3
3
|
require 'nxt_pipeline/version'
|
|
4
4
|
require 'nxt_pipeline/logger'
|
|
5
|
-
require 'nxt_pipeline/constructor'
|
|
6
5
|
require 'nxt_pipeline/pipeline'
|
|
7
6
|
require 'nxt_pipeline/step'
|
|
8
7
|
require 'nxt_pipeline/callbacks'
|
|
9
8
|
require 'nxt_pipeline/error_callback'
|
|
10
9
|
|
|
11
10
|
module NxtPipeline
|
|
11
|
+
class << self
|
|
12
|
+
delegate :new, :call, to: Pipeline
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configuration(name, &block)
|
|
16
|
+
@configurations ||= {}
|
|
17
|
+
|
|
18
|
+
if block_given?
|
|
19
|
+
raise ArgumentError, "Configuration already defined for #{name}" if @configurations[name].present?
|
|
20
|
+
@configurations[name] = block
|
|
21
|
+
else
|
|
22
|
+
@configurations.fetch(name)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def constructor(name, default: false, &block)
|
|
27
|
+
@constructors ||= {}
|
|
28
|
+
|
|
29
|
+
if block_given?
|
|
30
|
+
raise ArgumentError, "Constructor already defined for #{name}" if @constructors[name].present?
|
|
31
|
+
|
|
32
|
+
default_constructor_name(name) if default
|
|
33
|
+
@constructors[name] = block
|
|
34
|
+
else
|
|
35
|
+
@constructors.fetch(name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def default_constructor_name(name = nil)
|
|
40
|
+
if name.present?
|
|
41
|
+
raise ArgumentError, "Default constructor #{@default_constructor_name} defined already" if @default_constructor_name.present?
|
|
42
|
+
@default_constructor_name = name
|
|
43
|
+
else
|
|
44
|
+
@default_constructor_name
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module_function :configuration, :constructor, :default_constructor_name
|
|
12
49
|
end
|
metadata
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nxt_pipeline
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nils Sommer
|
|
8
8
|
- Andreas Robecke
|
|
9
9
|
- Raphael Kallensee
|
|
10
|
-
autorequire:
|
|
10
|
+
autorequire:
|
|
11
11
|
bindir: exe
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date:
|
|
13
|
+
date: 2022-05-24 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: activesupport
|
|
@@ -110,7 +110,7 @@ dependencies:
|
|
|
110
110
|
- - ">="
|
|
111
111
|
- !ruby/object:Gem::Version
|
|
112
112
|
version: '0'
|
|
113
|
-
description:
|
|
113
|
+
description:
|
|
114
114
|
email:
|
|
115
115
|
- mail@nilssommer.de
|
|
116
116
|
executables: []
|
|
@@ -136,7 +136,6 @@ files:
|
|
|
136
136
|
- bin/setup
|
|
137
137
|
- lib/nxt_pipeline.rb
|
|
138
138
|
- lib/nxt_pipeline/callbacks.rb
|
|
139
|
-
- lib/nxt_pipeline/constructor.rb
|
|
140
139
|
- lib/nxt_pipeline/error_callback.rb
|
|
141
140
|
- lib/nxt_pipeline/logger.rb
|
|
142
141
|
- lib/nxt_pipeline/pipeline.rb
|
|
@@ -150,7 +149,7 @@ metadata:
|
|
|
150
149
|
allowed_push_host: https://rubygems.org
|
|
151
150
|
homepage_uri: https://github.com/nxt-insurance/nxt_pipeline
|
|
152
151
|
source_code_uri: https://github.com/nxt-insurance/nxt_pipeline.git
|
|
153
|
-
post_install_message:
|
|
152
|
+
post_install_message:
|
|
154
153
|
rdoc_options: []
|
|
155
154
|
require_paths:
|
|
156
155
|
- lib
|
|
@@ -166,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
166
165
|
version: '0'
|
|
167
166
|
requirements: []
|
|
168
167
|
rubygems_version: 3.1.2
|
|
169
|
-
signing_key:
|
|
168
|
+
signing_key:
|
|
170
169
|
specification_version: 4
|
|
171
170
|
summary: DSL to build Pipeline with mountable Segments to process things.
|
|
172
171
|
test_files: []
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
module NxtPipeline
|
|
2
|
-
class Constructor
|
|
3
|
-
def initialize(name, **opts, &block)
|
|
4
|
-
@name = name
|
|
5
|
-
@block = block
|
|
6
|
-
@opts = opts
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
attr_reader :opts, :block
|
|
10
|
-
|
|
11
|
-
delegate :arity, to: :block
|
|
12
|
-
|
|
13
|
-
def call(*args, **opts, &block)
|
|
14
|
-
# ActiveSupport's #delegate does not properly handle keyword arg passing
|
|
15
|
-
# in the latest released version. Thefore we bypass delegation by reimplementing
|
|
16
|
-
# the method ourselves. This is already fixed in Rails master though.
|
|
17
|
-
self.block.call(*args, **opts, &block)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|