nxt_pipeline 0.3.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be4eec17fa6c2abc134a5e02ef10681e60397e1b358a54c593e38faabcaa28f0
4
- data.tar.gz: a109440aa292f3f33be8cc43b4ceadfef9b7e7869d8e701dd2a0afebf37f62d2
3
+ metadata.gz: 77672035013c0cfe29bb5c5057178a9822e25c5b02e83b87a8cbb8dadac1d687
4
+ data.tar.gz: bb097fa702a19a9756d86a2ff63792bed998d9f8c5425263864695606bfca70d
5
5
  SHA512:
6
- metadata.gz: 3fc83095718bc6e4e2a8d1827e3e59ab1e03abe3e5b14994a8aa2699f4a996bc72cb85439a52720df378310ec93fb8de4009f483ddec31fa2b06b6b8e7a6dd8d
7
- data.tar.gz: 6244dcbcea68037108f6b91f0d15489ebc61cbe03b950d3f4306b66f24f5fd849401124a7816a9b3a940eebb11dad73f13ce36718e25047bed1951d7b20a8efc
6
+ metadata.gz: 2d355345b5aadcb3d02895cd85196cc60b34caef8c919d11480a5a2cd429c5e32baa37c83f8e169ca316311735343297ba0a3b763044e12ac02c8349cc849929
7
+ data.tar.gz: eeaa40df4479d0b4b39e5cfe5fd89756d6681dc3d9e9f96e65f6f1e4f9793c588298c33c0fa446c0f7ddf73a3a9e4a43c5da5f371cf25928f18c80bed52e3e58
@@ -7,7 +7,9 @@ jobs:
7
7
  build:
8
8
  docker:
9
9
  # specify the version you desire here
10
- - image: circleci/ruby:2.6.1-node
10
+ - image: circleci/ruby:2.7.0-node
11
+ environment:
12
+ BUNDLER_VERSION: 2.1.4
11
13
 
12
14
  working_directory: ~/repo
13
15
 
@@ -19,6 +21,8 @@ jobs:
19
21
  keys:
20
22
  - v1-dependencies-{{ checksum "Gemfile.lock" }}
21
23
 
24
+ - run: gem install bundler --version $BUNDLER_VERSION
25
+
22
26
  - run:
23
27
  name: install dependencies
24
28
  command: |
data/.pryrc ADDED
@@ -0,0 +1,6 @@
1
+ if defined?(PryByebug)
2
+ Pry.commands.alias_command 'c', 'continue'
3
+ Pry.commands.alias_command 's', 'step'
4
+ Pry.commands.alias_command 'n', 'next'
5
+ Pry.commands.alias_command 'f', 'finish'
6
+ end
@@ -1 +1 @@
1
- 2.6.3
1
+ 2.7.0
@@ -1,4 +1,22 @@
1
- ## Unreleased
1
+ ## nxt_pipeline 1.0.0 (24.11.2020)
2
+
3
+ Replace after and before execute hooks with proper callbacks.
4
+ Introduce before, after and around step callbacks
5
+
6
+ ## nxt_pipeline 0.4.3 (October 20, 2020)
7
+
8
+ Add new attribute readers on step object.
9
+
10
+ After executing a step execution_finished_at execution_started_at and execution_duration
11
+ will be set and can be accessed via attribute readers.
12
+
13
+ ## nxt_pipeline 0.4.2 (October 12, 2020)
14
+
15
+ * Fix bug when registering an error without passing arguments in which case the callback didn't get executed. More info: https://github.com/nxt-insurance/nxt_pipeline/issues/39
16
+
17
+ ## nxt_pipeline 0.4.1 (March 13, 2020)
18
+
19
+ * Fix deprecation warnings for Ruby 2.7
2
20
 
3
21
  ## nxt_pipeline 0.2.0 (March 10, 2019)
4
22
 
@@ -14,7 +32,7 @@
14
32
  Renamed `NxtPipeline::Pipeline#burst?` to `NxtPipeline::Pipeline#failed?`.
15
33
  Renamed `NxtPipeline::Pipeline#burst_segment` to `NxtPipeline::Pipeline#failed_step`.
16
34
  Renamed `NxtPipeline::Pipeline::rescue_segment_burst` to `NxtPipeline::Pipeline::rescue_errors`.
17
-
35
+
18
36
  *Nils Sommer*
19
37
 
20
38
  * Setup [guard](https://github.com/guard/guard) to run specs upon file changes during development.
@@ -29,4 +47,4 @@
29
47
 
30
48
  * Initial Release.
31
49
 
32
- *Nils Sommer*
50
+ *Nils Sommer*
@@ -1,23 +1,26 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nxt_pipeline (0.3.1)
4
+ nxt_pipeline (1.0.0)
5
5
  activesupport
6
+ nxt_registry
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
10
- activesupport (5.2.3)
11
+ activesupport (6.1.1)
11
12
  concurrent-ruby (~> 1.0, >= 1.0.2)
12
- i18n (>= 0.7, < 2)
13
- minitest (~> 5.1)
14
- tzinfo (~> 1.1)
15
- coderay (1.1.2)
16
- concurrent-ruby (1.1.5)
17
- diff-lcs (1.3)
18
- ffi (1.11.1)
13
+ i18n (>= 1.6, < 2)
14
+ minitest (>= 5.1)
15
+ tzinfo (~> 2.0)
16
+ zeitwerk (~> 2.3)
17
+ byebug (11.1.3)
18
+ coderay (1.1.3)
19
+ concurrent-ruby (1.1.8)
20
+ diff-lcs (1.4.4)
21
+ ffi (1.14.2)
19
22
  formatador (0.2.5)
20
- guard (2.15.0)
23
+ guard (2.16.2)
21
24
  formatador (>= 0.2.4)
22
25
  listen (>= 2.7, < 4.0)
23
26
  lumberjack (>= 1.0.12, < 2.0)
@@ -31,59 +34,62 @@ GEM
31
34
  guard (~> 2.1)
32
35
  guard-compat (~> 1.1)
33
36
  rspec (>= 2.99.0, < 4.0)
34
- i18n (1.6.0)
37
+ i18n (1.8.7)
35
38
  concurrent-ruby (~> 1.0)
36
- listen (3.1.5)
37
- rb-fsevent (~> 0.9, >= 0.9.4)
38
- rb-inotify (~> 0.9, >= 0.9.7)
39
- ruby_dep (~> 1.2)
40
- lumberjack (1.0.13)
41
- method_source (0.9.2)
42
- minitest (5.11.3)
39
+ listen (3.4.1)
40
+ rb-fsevent (~> 0.10, >= 0.10.3)
41
+ rb-inotify (~> 0.9, >= 0.9.10)
42
+ lumberjack (1.2.8)
43
+ method_source (1.0.0)
44
+ minitest (5.14.3)
43
45
  nenv (0.3.0)
44
- notiffany (0.1.1)
46
+ notiffany (0.1.3)
45
47
  nenv (~> 0.1)
46
48
  shellany (~> 0.0)
47
- pry (0.12.2)
48
- coderay (~> 1.1.0)
49
- method_source (~> 0.9.0)
50
- rake (12.3.3)
51
- rb-fsevent (0.10.3)
52
- rb-inotify (0.10.0)
49
+ nxt_registry (0.3.6)
50
+ activesupport
51
+ pry (0.13.1)
52
+ coderay (~> 1.1)
53
+ method_source (~> 1.0)
54
+ pry-byebug (3.9.0)
55
+ byebug (~> 11.0)
56
+ pry (~> 0.13.0)
57
+ rake (13.0.3)
58
+ rb-fsevent (0.10.4)
59
+ rb-inotify (0.10.1)
53
60
  ffi (~> 1.0)
54
- rspec (3.8.0)
55
- rspec-core (~> 3.8.0)
56
- rspec-expectations (~> 3.8.0)
57
- rspec-mocks (~> 3.8.0)
58
- rspec-core (3.8.1)
59
- rspec-support (~> 3.8.0)
60
- rspec-expectations (3.8.4)
61
+ rspec (3.10.0)
62
+ rspec-core (~> 3.10.0)
63
+ rspec-expectations (~> 3.10.0)
64
+ rspec-mocks (~> 3.10.0)
65
+ rspec-core (3.10.1)
66
+ rspec-support (~> 3.10.0)
67
+ rspec-expectations (3.10.1)
61
68
  diff-lcs (>= 1.2.0, < 2.0)
62
- rspec-support (~> 3.8.0)
63
- rspec-mocks (3.8.1)
69
+ rspec-support (~> 3.10.0)
70
+ rspec-mocks (3.10.1)
64
71
  diff-lcs (>= 1.2.0, < 2.0)
65
- rspec-support (~> 3.8.0)
66
- rspec-support (3.8.2)
72
+ rspec-support (~> 3.10.0)
73
+ rspec-support (3.10.1)
67
74
  rspec_junit_formatter (0.4.1)
68
75
  rspec-core (>= 2, < 4, != 2.12.0)
69
- ruby_dep (1.5.0)
70
76
  shellany (0.0.1)
71
- thor (0.20.3)
72
- thread_safe (0.3.6)
73
- tzinfo (1.2.5)
74
- thread_safe (~> 0.1)
77
+ thor (1.1.0)
78
+ tzinfo (2.0.4)
79
+ concurrent-ruby (~> 1.0)
80
+ zeitwerk (2.4.2)
75
81
 
76
82
  PLATFORMS
77
83
  ruby
78
84
 
79
85
  DEPENDENCIES
80
- bundler (~> 1.17)
86
+ bundler (~> 2.1)
81
87
  guard-rspec
82
88
  nxt_pipeline!
83
- pry
84
- rake (~> 12.3)
89
+ pry-byebug
90
+ rake (~> 13.0)
85
91
  rspec (~> 3.0)
86
92
  rspec_junit_formatter
87
93
 
88
94
  BUNDLED WITH
89
- 1.17.2
95
+ 2.1.4
data/README.md CHANGED
@@ -24,35 +24,35 @@ Or install it yourself as:
24
24
 
25
25
  ### Constructors
26
26
 
27
- First you probably want to configure a pipeline so that it can execute your steps.
28
- Therefore you want to define constructors for your steps. Constructors take a name
27
+ First you probably want to configure a pipeline so that it can execute your steps.
28
+ Therefore you want to define constructors for your steps. Constructors take a name
29
29
  as the first argument and step options as the second. All step options are being exposed
30
- by the step yielded to the constructor.
30
+ by the step yielded to the constructor.
31
31
 
32
32
  ```ruby
33
33
  pipeline = NxtPipeline::Pipeline.new do |p|
34
34
  # Add a named constructor that will be used to execute your steps later
35
35
  # All options that you pass in your step will be available through accessors in your constructor
36
- # You can call :to_s on a step to set it by default. You can later overwrite at execution for each step if needed.
36
+ # You can call :to_s on a step to set it by default. You can later overwrite at execution for each step if needed.
37
37
  p.constructor(:service, default: true) do |step, arg:|
38
38
  step.to_s = step.service_class.to_s
39
39
  result = step.service_class.new(options: arg).call
40
40
  result && { arg: result }
41
41
  end
42
-
42
+
43
43
  p.constructor(:job) do |step, arg:|
44
44
  step.job_class.perform_later(*arg) && { arg: arg }
45
45
  end
46
46
  end
47
47
 
48
- # Once a pipeline was created you can still configure it
48
+ # Once a pipeline was created you can still configure it
49
49
  pipeline.constructor(:call) do |step, arg:|
50
50
  result = step.caller.new(arg).call
51
51
  result && { arg: result }
52
52
  end
53
53
 
54
- # same with block syntax
55
- # You can use this to split up execution from configuration
54
+ # same with block syntax
55
+ # You can use this to split up execution from configuration
56
56
  pipeline.configure do |p|
57
57
  p.constructor(:call) do |step, arg:|
58
58
  result = step.caller.new(arg).call
@@ -61,13 +61,13 @@ pipeline.configure do |p|
61
61
  end
62
62
  ```
63
63
 
64
- ### Defining steps
64
+ ### Defining steps
65
65
 
66
66
  Once your pipeline knows how to execute your steps you can add those.
67
67
 
68
68
  ```ruby
69
69
  pipeline.step :service, service_class: MyServiceClass, to_s: 'First step'
70
- pipeline.step service_class: MyOtherServiceClass, to_s: 'Second step'
70
+ pipeline.step service_class: MyOtherServiceClass, to_s: 'Second step'
71
71
  # ^ Since service is the default step you don't have to specify it the step type each time
72
72
  pipeline.step :job, job_class: MyJobClass # to_s is optional
73
73
  pipeline.step :job, job_class: MyOtherJobClass
@@ -82,19 +82,19 @@ end
82
82
  ```
83
83
 
84
84
  You can also define inline steps, meaning the block will be executed. When you do not provide a :to_s option, type
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.
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
87
 
88
88
  ### Execution
89
89
 
90
- You can then execute the steps with:
90
+ You can then execute the steps with:
91
91
 
92
92
  ```ruby
93
- pipeline.execute('initial argument')
93
+ pipeline.execute(arg: 'initial argument')
94
94
 
95
95
  # Or run the steps directly using block syntax
96
96
 
97
- pipeline.execute('initial argument') do |p|
97
+ pipeline.execute(arg: 'initial argument') do |p|
98
98
  p.step :service, service_class: MyServiceClass, to_s: 'First step'
99
99
  p.step :service, service_class: MyOtherServiceClass, to_s: 'Second step'
100
100
  p.step :job, job_class: MyJobClass # to_s is optional
@@ -106,18 +106,18 @@ end
106
106
  You can also directly execute a pipeline with:
107
107
 
108
108
  ```ruby
109
- NxtPipeline::Pipeline.execute('initial argument') do |p|
109
+ NxtPipeline::Pipeline.execute(arg: 'initial argument') do |p|
110
110
  p.step do |_, arg:|
111
111
  { arg: arg.upcase }
112
112
  end
113
113
  end
114
- ```
114
+ ```
115
115
 
116
- You can query the steps of your pipeline simply by calling `pipeline.steps`. A NxtPipeline::Step will provide you with
117
- an interface to it's type, options, status (:success, :skipped, :failed), result, error and the index in the pipeline.
116
+ You can query the steps of your pipeline simply by calling `pipeline.steps`. A NxtPipeline::Step will provide you with
117
+ an interface to it's type, options, status (:success, :skipped, :failed), execution_finished_at execution_started_at, execution_duration, result, error and the index in the pipeline.
118
118
 
119
119
  ```
120
- pipeline.steps.first
120
+ pipeline.steps.first
121
121
  # will give you something like this:
122
122
 
123
123
  #<NxtPipeline::Step:0x00007f83eb399448
@@ -128,20 +128,23 @@ pipeline.steps.first
128
128
  @opts={:to_s=>:transformer, :method=>:upcase},
129
129
  @result=nil,
130
130
  @status=nil,
131
- @type=:transformer>
132
- ```
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,>
135
+ ```
133
136
 
134
137
  ### Guard clauses
135
138
 
136
139
  You can also define guard clauses that take a proc to prevent the execution of a step.
137
- When the guard takes an argument the step argument is yielded.
140
+ When the guard takes an argument the step argument is yielded.
138
141
 
139
142
  ```ruby
140
- pipeline.execute('initial argument') do |p|
143
+ pipeline.execute(arg: 'initial argument') do |p|
141
144
  p.step :service, service_class: MyServiceClass, if: -> (arg:) { arg == 'initial argument' }
142
145
  p.step :service, service_class: MyOtherServiceClass, unless: -> { false }
143
146
  end
144
-
147
+
145
148
  ```
146
149
 
147
150
  ### Error callbacks
@@ -153,98 +156,89 @@ NxtPipeline::Pipeline.new do |p|
153
156
  p.step do |_, arg:|
154
157
  { arg: arg.upcase }
155
158
  end
156
-
159
+
157
160
  p.on_error MyCustomError do |step, opts, error|
158
161
  # First matching error callback will be executed!
159
162
  end
160
-
163
+
161
164
  p.on_errors ArgumentError, KeyError do |step, opts, error|
162
165
  # First matching error callback will be executed!
163
166
  end
164
-
167
+
165
168
  p.on_errors YetAnotherError, halt_on_error: false do |step, opts, error|
166
169
  # After executing the callback the pipeline will not halt but continue to
167
170
  # execute the next steps.
168
171
  end
169
-
172
+
170
173
  p.on_errors do |step, opts, error|
171
174
  # This will match all errors inheriting from StandardError
172
175
  end
173
176
  end
174
- ```
177
+ ```
175
178
 
176
- ### Before and After callbacks
179
+ ### Before, around and after callbacks
177
180
 
178
- You can also define callbacks that run before and after the `#execute` action. Both callback blocks get the pipeline instance (to access stuff like the `log`) and the argument of the pipeline yielded.
181
+ 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.
183
+
184
+ #### Step callbacks
179
185
 
180
186
  ```ruby
181
187
  NxtPipeline::Pipeline.new do |p|
182
- p.before_execute do |pipeline, arg:|
183
- # Will be called from within #execute before entering the first step
184
- # After any configure block though!
188
+ p.before_step do |_, change_set|
189
+ change_set[:acc] << 'before step 1'
190
+ change_set
185
191
  end
186
-
187
- p.after_execute do |pipeline, arg:|
188
- # Will be called from within #execute after executing last step
192
+
193
+ p.around_step do |_, change_set, execution|
194
+ change_set[:acc] << 'around step 1'
195
+ execution.call # you have to specify where in your callback you want to call the inner block
196
+ change_set[:acc] << 'around step 1'
197
+ change_set
198
+ end
199
+
200
+ p.after_step do |_, change_set|
201
+ change_set[:acc] << 'after step 1'
202
+ change_set
189
203
  end
190
204
  end
191
205
  ```
192
206
 
193
- Note that the `after_execute` callback will not be called, when an error is raised in one of the steps. See the previous section (_Error callbacks_) for how to define callbacks that run in case of errors.
194
-
195
- ### DSL
196
-
197
- The gem also comes with an easy DSL to make pipeline handling in your code more convenient.
198
- Simply include NxtPipeline::Dsl in your class:
207
+ #### Execution callbacks
199
208
 
200
209
  ```ruby
201
- class MyAwesomeClass
202
- include NxtPipeline::Dsl
203
-
204
- # register a pipeline with a name and a block
205
- pipeline :validation do |p|
206
- pipeline.constructor(:validate) do |step, arg:|
207
- result = step.validator.call(arg: arg)
208
- result && { arg: result }
209
- end
210
-
211
- pipeline.step :validate, validator: NameValidator
212
- pipeline.step :validate, validator: AdressValidator
213
- pipeline.step :validate, validator: BankAccountValidator
214
- pipeline.step :validate, validator: PhoneNumberValidator
215
-
216
- p.on_error ValidationError do |step, opts, error|
217
- # ...
218
- end
219
- end
220
-
221
- pipeline :execution do |p|
222
- p.step do |_, arg:|
223
- { arg: arg.upcase }
224
- end
225
-
226
- p.on_error MyCustomError do |step, opts, error|
227
- # nesting pipelines also works
228
- pipeline(:error).execute(error)
229
- end
210
+ NxtPipeline::Pipeline.new do |p|
211
+ p.before_execution do |_, change_set|
212
+ change_set[:acc] << 'before execution 1'
213
+ change_set
230
214
  end
231
-
232
- pipeline :error do |p|
233
- p.step do |_, error|
234
- error # do something here
235
- end
215
+
216
+ p.around_execution do |_, change_set, execution|
217
+ change_set[:acc] << 'around execution 1'
218
+ execution.call # you have to specify where in your callback you want to call the inner block
219
+ change_set[:acc] << 'around execution 1'
220
+ change_set
236
221
  end
237
-
238
- def call(arg)
239
- # execute a pipeline simply by fetching it and calling execute on it as you would normally
240
- pipeline(:execution).execute(arg: arg)
222
+
223
+ p.after_execution do |_, change_set|
224
+ change_set[:acc] << 'after execution 1'
225
+ change_set
241
226
  end
242
227
  end
243
228
  ```
244
229
 
230
+ Note that the `after_execute` callback will not be called in case a step raises an error.
231
+ See the previous section (_Error callbacks_) for how to define callbacks that run in case of errors.
232
+
233
+ ### Step resolvers
234
+
235
+ NxtPipeline is using so called step_resolvers to find the constructor for a given step by the arguments passed in.
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.
238
+
239
+
245
240
  ## Topics
246
- - Step orchestration (chainable steps)
247
- - Constructors should take arg as first and step as second arg
241
+ - Constructors should take arg as first and step as second arg
248
242
 
249
243
  ## Development
250
244
 
@@ -1,11 +1,12 @@
1
1
  require 'active_support/all'
2
+ require 'nxt_registry'
2
3
  require 'nxt_pipeline/version'
3
4
  require 'nxt_pipeline/logger'
4
5
  require 'nxt_pipeline/constructor'
5
6
  require 'nxt_pipeline/pipeline'
6
7
  require 'nxt_pipeline/step'
8
+ require 'nxt_pipeline/callbacks'
7
9
  require 'nxt_pipeline/error_callback'
8
- require 'nxt_pipeline/dsl'
9
10
 
10
11
  module NxtPipeline
11
12
  end
@@ -0,0 +1,58 @@
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
+ registry.resolve!(kind_of_callback, type).each do |callback|
14
+ run_callback(callback, change_set)
15
+ end
16
+ end
17
+
18
+ def around(type, change_set, &execution)
19
+ around_callbacks = registry.resolve!(:around, type)
20
+ return execution.call unless around_callbacks.any?
21
+
22
+ callback_chain = around_callbacks.reverse.inject(execution) do |previous, callback|
23
+ -> { callback.call(pipeline, change_set, previous) }
24
+ end
25
+
26
+ callback_chain.call
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :registry, :pipeline
32
+
33
+ def run_callback(callback, change_set)
34
+ args = [pipeline, change_set]
35
+ args = args.take(callback.arity)
36
+ callback.call(*args)
37
+ end
38
+
39
+ def build_registry
40
+ NxtRegistry::Registry.new(:callbacks) do
41
+ register(:before) do
42
+ register(:step, [])
43
+ register(:execution, [])
44
+ end
45
+
46
+ register(:around) do
47
+ register(:step, [])
48
+ register(:execution, [])
49
+ end
50
+
51
+ register(:after) do
52
+ register(:step, [])
53
+ register(:execution, [])
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,13 +1,20 @@
1
1
  module NxtPipeline
2
2
  class Constructor
3
- def initialize(name, opts, block)
3
+ def initialize(name, **opts, &block)
4
4
  @name = name
5
5
  @block = block
6
6
  @opts = opts
7
7
  end
8
8
 
9
9
  attr_reader :opts, :block
10
-
11
- delegate :call, :arity, to: :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
12
19
  end
13
20
  end
@@ -1,7 +1,7 @@
1
1
  module NxtPipeline
2
2
  class ErrorCallback
3
- def initialize(errors, halt_on_error, callback)
4
- @errors = errors
3
+ def initialize(errors, halt_on_error, &callback)
4
+ @errors = errors.any? ? errors : [StandardError]
5
5
  @halt_on_error = halt_on_error
6
6
  @callback = callback
7
7
  end
@@ -4,14 +4,15 @@ module NxtPipeline
4
4
  new(&block).execute(**opts)
5
5
  end
6
6
 
7
- def initialize(&block)
7
+ def initialize(step_resolvers = default_step_resolvers, &block)
8
8
  @steps = []
9
9
  @error_callbacks = []
10
10
  @logger = Logger.new
11
11
  @current_step = nil
12
12
  @current_arg = nil
13
13
  @default_constructor_name = nil
14
- @registry = {}
14
+ @constructors = {}
15
+ @step_resolvers = step_resolvers
15
16
 
16
17
  configure(&block) if block_given?
17
18
  end
@@ -22,68 +23,75 @@ module NxtPipeline
22
23
 
23
24
  def constructor(name, **opts, &constructor)
24
25
  name = name.to_sym
25
- raise StandardError, "Already registered step :#{name}" if registry[name]
26
+ raise StandardError, "Already registered step :#{name}" if constructors[name]
26
27
 
27
- registry[name] = Constructor.new(name, opts, constructor)
28
+ constructors[name] = Constructor.new(name, **opts, &constructor)
28
29
 
29
30
  return unless opts.fetch(:default, false)
30
31
  set_default_constructor(name)
31
32
  end
32
33
 
33
- def set_default_constructor(name)
34
+ def step_resolver(&block)
35
+ step_resolvers << block
36
+ end
37
+
38
+ def set_default_constructor(default_constructor)
34
39
  raise_duplicate_default_constructor if default_constructor_name.present?
35
- self.default_constructor_name = name
40
+ self.default_constructor_name = default_constructor
36
41
  end
37
42
 
38
43
  def raise_duplicate_default_constructor
39
44
  raise ArgumentError, 'Default step already defined'
40
45
  end
41
46
 
42
- def step(type = nil, **opts, &block)
43
- type = type&.to_sym
44
-
47
+ def step(argument = nil, **opts, &block)
45
48
  constructor = if block_given?
46
- # make type the :to_s of inline steps
47
- # fall back to :inline if no type is given
48
- type ||= :inline
49
- opts.reverse_merge!(to_s: type)
50
- Constructor.new(type, opts, block)
49
+ # make type the :to_s of inline steps fall back to :inline if no type is given
50
+ argument ||= :inline
51
+ opts.reverse_merge!(to_s: argument)
52
+ Constructor.new(:inline, **opts, &block)
51
53
  else
52
- if type
53
- raise_reserved_type_inline_error if type == :inline
54
- registry.fetch(type) { raise KeyError, "No step :#{type} registered" }
54
+ constructor = step_resolvers.lazy.map do |resolver|
55
+ resolver.call(argument)
56
+ end.find(&:itself)
57
+
58
+ if constructor
59
+ constructor && constructors.fetch(constructor) { raise KeyError, "No step :#{argument} registered" }
60
+ elsif default_constructor
61
+ argument ||= default_constructor_name
62
+ default_constructor
55
63
  else
56
- # When no type was given we try to fallback to the type of the default constructor
57
- type = default_constructor_name
58
- # If none was given - raise
59
- default_constructor || (raise StandardError, 'No default step registered')
64
+ raise StandardError, "Could not resolve step from: #{argument}"
60
65
  end
61
66
  end
62
67
 
63
- steps << Step.new(type, constructor, steps.count, **opts)
68
+ register_step(argument, constructor, callbacks, **opts)
64
69
  end
65
70
 
66
- def execute(**opts, &block)
71
+ def execute(**change_set, &block)
67
72
  reset
68
73
 
69
74
  configure(&block) if block_given?
70
- before_execute_callback.call(self, opts) if before_execute_callback.respond_to?(:call)
71
-
72
- result = steps.inject(opts) do |options, step|
73
- execute_step(step, **options)
74
- rescue StandardError => error
75
- callback = find_error_callback(error)
76
- raise unless callback && callback.continue_after_error?
77
- handle_step_error(error)
78
- options
75
+ callbacks.run(:before, :execution, change_set)
76
+
77
+ result = callbacks.around :execution, change_set do
78
+ steps.inject(change_set) do |set, step|
79
+ execute_step(step, **set)
80
+ rescue StandardError => error
81
+ decorate_error_with_details(error, set, step, logger)
82
+ handle_error_of_step(error)
83
+ set
84
+ end
79
85
  end
80
86
 
81
- after_execute_callback.call(self, result) if after_execute_callback.respond_to?(:call)
87
+ callbacks.run(:after, :execution, change_set)
82
88
  result
83
89
  rescue StandardError => error
84
90
  handle_step_error(error)
85
91
  end
86
92
 
93
+ alias_method :call, :execute
94
+
87
95
  def handle_step_error(error)
88
96
  log_step(current_step)
89
97
  callback = find_error_callback(error)
@@ -94,40 +102,58 @@ module NxtPipeline
94
102
  end
95
103
 
96
104
  def on_errors(*errors, halt_on_error: true, &callback)
97
- error_callbacks << ErrorCallback.new(errors, halt_on_error, callback)
105
+ error_callbacks << ErrorCallback.new(errors, halt_on_error, &callback)
98
106
  end
99
107
 
100
108
  alias :on_error :on_errors
101
109
 
102
- def before_execute(&callback)
103
- self.before_execute_callback = callback
110
+ def before_step(&block)
111
+ callbacks.register([:before, :step], block)
112
+ end
113
+
114
+ def after_step(&block)
115
+ callbacks.register([:after, :step], block)
104
116
  end
105
117
 
106
- def after_execute(&callback)
107
- self.after_execute_callback = callback
118
+ def around_step(&block)
119
+ callbacks.register([:around, :step], block)
120
+ end
121
+
122
+ def before_execution(&block)
123
+ callbacks.register([:before, :execution], block)
124
+ end
125
+
126
+ def after_execution(&block)
127
+ callbacks.register([:after, :execution], block)
128
+ end
129
+
130
+ def around_execution(&block)
131
+ callbacks.register([:around, :execution], block)
108
132
  end
109
133
 
110
134
  private
111
135
 
112
- attr_reader :error_callbacks, :registry
136
+ def callbacks
137
+ @callbacks ||= NxtPipeline::Callbacks.new(pipeline: self)
138
+ end
139
+
140
+ attr_reader :error_callbacks, :constructors, :step_resolvers
113
141
  attr_accessor :current_step,
114
142
  :current_arg,
115
- :default_constructor_name,
116
- :before_execute_callback,
117
- :after_execute_callback
143
+ :default_constructor_name
118
144
 
119
145
  def default_constructor
120
146
  return unless default_constructor_name
121
147
 
122
- @default_constructor ||= registry[default_constructor_name.to_sym]
148
+ @default_constructor ||= constructors[default_constructor_name.to_sym]
123
149
  end
124
150
 
125
- def execute_step(step, **opts)
151
+ def execute_step(step, **change_set)
126
152
  self.current_step = step
127
- self.current_arg = opts
128
- result = step.execute(**opts)
153
+ self.current_arg = change_set
154
+ result = step.execute(**change_set)
129
155
  log_step(step)
130
- result || opts
156
+ result || change_set
131
157
  end
132
158
 
133
159
  def find_error_callback(error)
@@ -148,5 +174,34 @@ module NxtPipeline
148
174
  def raise_reserved_type_inline_error
149
175
  raise ArgumentError, 'Type :inline is reserved for inline steps!'
150
176
  end
177
+
178
+ def default_step_resolvers
179
+ [->(step_argument) { step_argument.is_a?(Symbol) && step_argument }]
180
+ end
181
+
182
+ def decorate_error_with_details(error, change_set, step, logger)
183
+ error.define_singleton_method :details do
184
+ OpenStruct.new(
185
+ change_set: change_set,
186
+ logger: logger,
187
+ step: step
188
+ )
189
+ end
190
+ error
191
+ end
192
+
193
+ def register_step(argument, constructor, callbacks, **opts)
194
+ steps << Step.new(argument, constructor, steps.count, self, callbacks, **opts)
195
+ end
196
+
197
+ def handle_error_of_step(error)
198
+ error_callback = find_error_callback(error)
199
+ raise error unless error_callback.present? && error_callback.continue_after_error?
200
+
201
+ log_step(current_step)
202
+ raise error unless error_callback.present?
203
+
204
+ error_callback.call(current_step, current_arg, error)
205
+ end
151
206
  end
152
207
  end
@@ -1,50 +1,96 @@
1
1
  module NxtPipeline
2
2
  class Step
3
- def initialize(type, constructor, index, **opts)
3
+ def initialize(argument, constructor, index, pipeline, callbacks, **opts)
4
4
  define_attr_readers(opts)
5
5
 
6
- @type = type
6
+ @pipeline = pipeline
7
+ @callbacks = callbacks
8
+ @argument = argument
7
9
  @index = index
8
10
  @opts = opts
9
11
  @constructor = constructor
10
- @to_s = "#{opts.merge(type: type)}"
12
+ @to_s = "#{opts.merge(argument: argument)}"
13
+ @options_mapper = opts[:map_options]
11
14
 
12
15
  @status = nil
13
16
  @result = nil
14
17
  @error = nil
18
+ @mapped_options = nil
15
19
  end
16
20
 
17
- attr_reader :type, :result, :status, :error, :opts, :index
21
+ attr_reader :argument,
22
+ :result,
23
+ :status,
24
+ :execution_started_at,
25
+ :execution_finished_at,
26
+ :execution_duration,
27
+ :error,
28
+ :opts,
29
+ :index,
30
+ :mapped_options
31
+
18
32
  attr_accessor :to_s
19
33
 
20
34
  alias_method :name=, :to_s=
21
35
  alias_method :name, :to_s
22
36
 
23
- def execute(**opts)
24
- guard_args = [opts, self]
25
- if_guard_args = guard_args.take(if_guard.arity)
26
- unless_guard_guard_args = guard_args.take(unless_guard.arity)
37
+ def execute(**change_set)
38
+ track_execution_time do
39
+ set_mapped_options(change_set)
40
+ guard_args = [change_set, self]
27
41
 
28
- if !unless_guard.call(*unless_guard_guard_args) && if_guard.call(*if_guard_args)
29
- self.result = constructor.call(self, **opts)
30
- end
42
+ callbacks.run(:before, :step, change_set)
43
+
44
+ if evaluate_unless_guard(guard_args) && evaluate_if_guard(guard_args)
45
+ callbacks.around(:step, change_set) do
46
+ set_result(change_set)
47
+ end
48
+ end
31
49
 
32
- set_status
33
- result
50
+ callbacks.run(:after, :step, change_set)
51
+
52
+ set_status
53
+ result
54
+ end
34
55
  rescue StandardError => e
35
56
  self.status = :failed
36
57
  self.error = e
37
58
  raise
38
59
  end
39
60
 
40
- def type?(potential_type)
41
- type.to_sym == potential_type.to_sym
61
+ def set_mapped_options(change_set)
62
+ mapper = options_mapper || default_options_mapper
63
+ mapper_args = [change_set, self].take(mapper.arity)
64
+ self.mapped_options = mapper.call(*mapper_args)
42
65
  end
43
66
 
44
67
  private
45
68
 
46
- attr_writer :result, :status, :error
47
- attr_reader :constructor
69
+ attr_writer :result, :status, :error, :mapped_options, :execution_started_at, :execution_finished_at, :execution_duration
70
+ attr_reader :constructor, :options_mapper, :pipeline, :callbacks
71
+
72
+ def evaluate_if_guard(args)
73
+ execute_callable(if_guard, args)
74
+ end
75
+
76
+ def evaluate_unless_guard(args)
77
+ !execute_callable(unless_guard, args)
78
+ end
79
+
80
+ def set_result(change_set)
81
+ args = [self, change_set]
82
+ self.result = execute_callable(constructor, args)
83
+ end
84
+
85
+ def execute_callable(callable, args)
86
+ args = args.take(callable.arity)
87
+
88
+ if args.last.is_a?(Hash)
89
+ callable.call(*args.take(args.length - 1), **args.last)
90
+ else
91
+ callable.call(*args)
92
+ end
93
+ end
48
94
 
49
95
  def if_guard
50
96
  opts.fetch(:if) { guard(true) }
@@ -69,5 +115,30 @@ module NxtPipeline
69
115
  def set_status
70
116
  self.status = result.present? ? :success : :skipped
71
117
  end
118
+
119
+ def track_execution_time(&block)
120
+ set_execution_started_at
121
+ block.call
122
+ ensure
123
+ set_execution_finished_at
124
+ set_execution_duration
125
+ end
126
+
127
+ def set_execution_started_at
128
+ self.execution_started_at = Time.current
129
+ end
130
+
131
+ def set_execution_finished_at
132
+ self.execution_finished_at = Time.current
133
+ end
134
+
135
+ def set_execution_duration
136
+ self.execution_duration = execution_finished_at - execution_started_at
137
+ end
138
+
139
+ def default_options_mapper
140
+ # returns an empty hash
141
+ ->(_) { {} }
142
+ end
72
143
  end
73
144
  end
@@ -1,3 +1,4 @@
1
1
  module NxtPipeline
2
- VERSION = "0.3.1".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
4
+
@@ -35,9 +35,10 @@ Gem::Specification.new do |spec|
35
35
  spec.require_paths = ["lib"]
36
36
 
37
37
  spec.add_dependency "activesupport"
38
- spec.add_development_dependency "bundler", "~> 1.17"
38
+ spec.add_dependency "nxt_registry"
39
+ spec.add_development_dependency "bundler", "~> 2.1"
39
40
  spec.add_development_dependency "guard-rspec"
40
- spec.add_development_dependency "rake", "~> 12.3"
41
+ spec.add_development_dependency "rake", "~> 13.0"
41
42
  spec.add_development_dependency "rspec", "~> 3.0"
42
- spec.add_development_dependency "pry"
43
+ spec.add_development_dependency "pry-byebug"
43
44
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nxt_pipeline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nils Sommer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2019-08-13 00:00:00.000000000 Z
13
+ date: 2021-01-20 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -26,20 +26,34 @@ dependencies:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
28
  version: '0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: nxt_registry
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
29
43
  - !ruby/object:Gem::Dependency
30
44
  name: bundler
31
45
  requirement: !ruby/object:Gem::Requirement
32
46
  requirements:
33
47
  - - "~>"
34
48
  - !ruby/object:Gem::Version
35
- version: '1.17'
49
+ version: '2.1'
36
50
  type: :development
37
51
  prerelease: false
38
52
  version_requirements: !ruby/object:Gem::Requirement
39
53
  requirements:
40
54
  - - "~>"
41
55
  - !ruby/object:Gem::Version
42
- version: '1.17'
56
+ version: '2.1'
43
57
  - !ruby/object:Gem::Dependency
44
58
  name: guard-rspec
45
59
  requirement: !ruby/object:Gem::Requirement
@@ -60,14 +74,14 @@ dependencies:
60
74
  requirements:
61
75
  - - "~>"
62
76
  - !ruby/object:Gem::Version
63
- version: '12.3'
77
+ version: '13.0'
64
78
  type: :development
65
79
  prerelease: false
66
80
  version_requirements: !ruby/object:Gem::Requirement
67
81
  requirements:
68
82
  - - "~>"
69
83
  - !ruby/object:Gem::Version
70
- version: '12.3'
84
+ version: '13.0'
71
85
  - !ruby/object:Gem::Dependency
72
86
  name: rspec
73
87
  requirement: !ruby/object:Gem::Requirement
@@ -83,7 +97,7 @@ dependencies:
83
97
  - !ruby/object:Gem::Version
84
98
  version: '3.0'
85
99
  - !ruby/object:Gem::Dependency
86
- name: pry
100
+ name: pry-byebug
87
101
  requirement: !ruby/object:Gem::Requirement
88
102
  requirements:
89
103
  - - ">="
@@ -105,6 +119,7 @@ extra_rdoc_files: []
105
119
  files:
106
120
  - ".circleci/config.yml"
107
121
  - ".gitignore"
122
+ - ".pryrc"
108
123
  - ".rspec"
109
124
  - ".ruby-version"
110
125
  - ".travis.yml"
@@ -120,8 +135,8 @@ files:
120
135
  - bin/rspec
121
136
  - bin/setup
122
137
  - lib/nxt_pipeline.rb
138
+ - lib/nxt_pipeline/callbacks.rb
123
139
  - lib/nxt_pipeline/constructor.rb
124
- - lib/nxt_pipeline/dsl.rb
125
140
  - lib/nxt_pipeline/error_callback.rb
126
141
  - lib/nxt_pipeline/logger.rb
127
142
  - lib/nxt_pipeline/pipeline.rb
@@ -150,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
165
  - !ruby/object:Gem::Version
151
166
  version: '0'
152
167
  requirements: []
153
- rubygems_version: 3.0.3
168
+ rubygems_version: 3.1.2
154
169
  signing_key:
155
170
  specification_version: 4
156
171
  summary: DSL to build Pipeline with mountable Segments to process things.
@@ -1,44 +0,0 @@
1
- module NxtPipeline
2
- module Dsl
3
- module ClassMethods
4
- def pipeline(name = :default, &block)
5
- name = name.to_sym
6
-
7
- if block_given?
8
- raise_already_registered_error(name) if pipeline_registry.key?(name)
9
- pipeline_registry[name] = block
10
- else
11
- config = pipeline_registry.fetch(name) { raise KeyError, "No pipeline #{name} registered"}
12
- NxtPipeline::Pipeline.new(&config)
13
- end
14
- end
15
-
16
- def pipeline!(name, &block)
17
- raise ArgumentError, "No block given!" unless block_given?
18
- pipeline_registry[name] = block
19
- end
20
-
21
- private
22
-
23
- def inherited(child)
24
- child.instance_variable_set(:@pipeline_registry, pipeline_registry.clone)
25
- end
26
-
27
- def raise_already_registered_error(name)
28
- raise KeyError, "Already registered a pipeline #{name}. Call pipeline! to overwrite already registered pipelines"
29
- end
30
-
31
- def pipeline_registry
32
- @pipeline_registry ||= ActiveSupport::HashWithIndifferentAccess.new
33
- end
34
- end
35
-
36
- def self.included(base)
37
- base.extend(ClassMethods)
38
-
39
- def pipeline(name = :default)
40
- self.class.pipeline(name)
41
- end
42
- end
43
- end
44
- end