nxt_pipeline 0.4.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,10 @@
1
1
  module NxtPipeline
2
2
  class Pipeline
3
- def self.execute(**opts, &block)
4
- new(&block).execute(**opts)
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(step_resolvers = default_step_resolvers, &block)
7
+ def initialize(configuration: nil, resolvers: [], &block)
8
8
  @steps = []
9
9
  @error_callbacks = []
10
10
  @logger = Logger.new
@@ -12,90 +12,156 @@ module NxtPipeline
12
12
  @current_arg = nil
13
13
  @default_constructor_name = nil
14
14
  @constructors = {}
15
- @step_resolvers = step_resolvers
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, :steps
28
+ attr_accessor :logger
29
+ attr_reader :result
23
30
 
24
- def constructor(name, **opts, &constructor)
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] = Constructor.new(name, **opts, &constructor)
35
+ constructors[name] = constructor
29
36
 
30
- return unless opts.fetch(:default, false)
37
+ return unless default
31
38
  set_default_constructor(name)
32
39
  end
33
40
 
34
- def step_resolver(&block)
35
- step_resolvers << block
41
+ def constructor_resolver(&block)
42
+ constructor_resolvers << block
36
43
  end
37
44
 
38
- def set_default_constructor(name)
45
+ def set_default_constructor(default_constructor)
39
46
  raise_duplicate_default_constructor if default_constructor_name.present?
40
- self.default_constructor_name = name
47
+ self.default_constructor_name = default_constructor
41
48
  end
42
49
 
43
50
  def raise_duplicate_default_constructor
44
51
  raise ArgumentError, 'Default step already defined'
45
52
  end
46
53
 
47
- def step(argument = nil, **opts, &block)
48
- constructor = if block_given?
49
- # make type the :to_s of inline steps
50
- # fall back to :inline if no type is given
51
- argument ||= :inline
52
- opts.reverse_merge!(to_s: argument)
53
- Constructor.new(:inline, **opts, &block)
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
54
110
  else
55
- constructor = step_resolvers.lazy.map do |resolver|
56
- resolver.call(argument)
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)
57
116
  end.find(&:itself)
58
117
 
59
- if constructor
60
- constructor && constructors.fetch(constructor) { raise KeyError, "No step :#{argument} registered" }
61
- elsif default_constructor
62
- argument ||= default_constructor_name
63
- default_constructor
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]
64
123
  else
65
- raise StandardError, "Could not resolve step from: #{argument}"
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
66
141
  end
67
142
  end
68
143
 
69
- steps << Step.new(argument, constructor, steps.count, **opts)
144
+ register_step(argument, resolved_constructor, callbacks, **opts)
70
145
  end
71
146
 
72
- def execute(**changeset, &block)
147
+ def call(acc, &block)
73
148
  reset
74
149
 
75
150
  configure(&block) if block_given?
76
- before_execute_callback.call(self, changeset) if before_execute_callback.respond_to?(:call)
77
-
78
- result = steps.inject(changeset) do |changeset, step|
79
- execute_step(step, **changeset)
80
- rescue StandardError => error
81
- logger_for_error = logger
82
-
83
- error.define_singleton_method :details do
84
- OpenStruct.new(
85
- changeset: changeset,
86
- logger: logger_for_error,
87
- step: step
88
- )
151
+ callbacks.run(:before, :execution, acc)
152
+
153
+ self.result = callbacks.around :execution, acc do
154
+ steps.inject(acc) do |changes, step|
155
+ self.result = call_step(step, changes)
156
+ rescue StandardError => error
157
+ decorate_error_with_details(error, changes, step, logger)
158
+ handle_error_of_step(step, error)
159
+ result
89
160
  end
90
-
91
- callback = find_error_callback(error)
92
- raise unless callback && callback.continue_after_error?
93
- handle_step_error(error)
94
- changeset
95
161
  end
96
162
 
97
- after_execute_callback.call(self, result) if after_execute_callback.respond_to?(:call)
98
- result
163
+ # callbacks.run(:after, :execution, result) TODO: Better pass result to callback?
164
+ self.result = callbacks.run(:after, :execution, acc) || result
99
165
  rescue StandardError => error
100
166
  handle_step_error(error)
101
167
  end
@@ -106,7 +172,7 @@ module NxtPipeline
106
172
 
107
173
  raise unless callback
108
174
 
109
- callback.call(current_step, current_arg, error)
175
+ callback.call(error, current_arg, current_step)
110
176
  end
111
177
 
112
178
  def on_errors(*errors, halt_on_error: true, &callback)
@@ -115,22 +181,41 @@ module NxtPipeline
115
181
 
116
182
  alias :on_error :on_errors
117
183
 
118
- def before_execute(&callback)
119
- self.before_execute_callback = callback
184
+ def before_step(&block)
185
+ callbacks.register([:before, :step], block)
186
+ end
187
+
188
+ def after_step(&block)
189
+ callbacks.register([:after, :step], block)
190
+ end
191
+
192
+ def around_step(&block)
193
+ callbacks.register([:around, :step], block)
120
194
  end
121
195
 
122
- def after_execute(&callback)
123
- self.after_execute_callback = callback
196
+ def before_execution(&block)
197
+ callbacks.register([:before, :execution], block)
198
+ end
199
+
200
+ def after_execution(&block)
201
+ callbacks.register([:after, :execution], block)
202
+ end
203
+
204
+ def around_execution(&block)
205
+ callbacks.register([:around, :execution], block)
124
206
  end
125
207
 
126
208
  private
127
209
 
128
- attr_reader :error_callbacks, :constructors, :step_resolvers
129
- attr_accessor :current_step,
130
- :current_arg,
131
- :default_constructor_name,
132
- :before_execute_callback,
133
- :after_execute_callback
210
+ attr_writer :result
211
+
212
+ def callbacks
213
+ @callbacks ||= NxtPipeline::Callbacks.new(pipeline: self)
214
+ end
215
+
216
+ attr_reader :error_callbacks, :constructors, :constructor_resolvers
217
+ attr_writer :default_constructor_name
218
+ attr_accessor :current_step, :current_arg
134
219
 
135
220
  def default_constructor
136
221
  return unless default_constructor_name
@@ -138,12 +223,12 @@ module NxtPipeline
138
223
  @default_constructor ||= constructors[default_constructor_name.to_sym]
139
224
  end
140
225
 
141
- def execute_step(step, **changeset)
226
+ def call_step(step, acc)
142
227
  self.current_step = step
143
- self.current_arg = changeset
144
- result = step.execute(**changeset)
228
+ self.current_arg = acc
229
+ result = step.call(acc)
145
230
  log_step(step)
146
- result || changeset
231
+ result || acc
147
232
  end
148
233
 
149
234
  def find_error_callback(error)
@@ -161,12 +246,34 @@ module NxtPipeline
161
246
  self.current_step = nil
162
247
  end
163
248
 
164
- def raise_reserved_type_inline_error
165
- raise ArgumentError, 'Type :inline is reserved for inline steps!'
249
+
250
+ def decorate_error_with_details(error, change_set, step, logger)
251
+ error.define_singleton_method :details do
252
+ OpenStruct.new(
253
+ change_set: change_set,
254
+ logger: logger,
255
+ step: step
256
+ )
257
+ end
258
+ error
259
+ end
260
+
261
+ def register_step(argument, constructor, callbacks, **opts)
262
+ steps << Step.new(argument, constructor, steps.count, self, callbacks, **opts)
263
+ end
264
+
265
+ def handle_error_of_step(step, error)
266
+ error_callback = find_error_callback(error)
267
+ raise error unless error_callback.present? && error_callback.continue_after_error?
268
+
269
+ log_step(step)
270
+ raise error unless error_callback.present?
271
+
272
+ error_callback.call(error, current_arg, step)
166
273
  end
167
274
 
168
- def default_step_resolvers
169
- [->(step_argument) { step_argument.is_a?(Symbol) && step_argument }]
275
+ def default_constructor_name
276
+ @default_constructor_name || ::NxtPipeline.default_constructor_name
170
277
  end
171
278
  end
172
279
  end
@@ -1,67 +1,96 @@
1
1
  module NxtPipeline
2
2
  class Step
3
- def initialize(argument, constructor, index, **opts)
4
- define_attr_readers(opts)
3
+ RESERVED_OPTION_KEYS = %i[to_s unless if]
5
4
 
5
+ def initialize(argument, constructor, index, pipeline, callbacks, **opts)
6
+ @opts = opts.symbolize_keys
7
+
8
+ @pipeline = pipeline
9
+ @callbacks = callbacks
6
10
  @argument = argument
7
11
  @index = index
8
- @opts = opts
9
12
  @constructor = constructor
10
- @to_s = "#{opts.merge(argument: argument)}"
13
+ @to_s = opts.fetch(:to_s) { argument }
11
14
  @options_mapper = opts[:map_options]
12
15
 
13
16
  @status = nil
14
17
  @result = nil
15
18
  @error = nil
16
19
  @mapped_options = nil
20
+ @meta_data = nil
21
+
22
+ define_option_readers
17
23
  end
18
24
 
19
- attr_reader :argument, :result, :status, :error, :opts, :index, :mapped_options
20
- attr_accessor :to_s
25
+ attr_reader :argument,
26
+ :result,
27
+ :execution_started_at,
28
+ :execution_finished_at,
29
+ :execution_duration,
30
+ :error,
31
+ :opts,
32
+ :index,
33
+ :mapped_options
34
+
35
+ attr_writer :to_s
36
+ attr_accessor :meta_data, :status
37
+
38
+ def call(acc)
39
+ track_execution_time do
40
+ set_mapped_options(acc)
41
+ guard_args = [acc, self]
42
+
43
+ callbacks.run(:before, :step, acc)
44
+
45
+ if evaluate_unless_guard(guard_args) && evaluate_if_guard(guard_args)
46
+ callbacks.around(:step, acc) do
47
+ set_result(acc)
48
+ end
49
+ end
21
50
 
22
- alias_method :name=, :to_s=
23
- alias_method :name, :to_s
51
+ callbacks.run(:after, :step, acc)
52
+
53
+ set_status
54
+ result
55
+ end
56
+ rescue StandardError => e
57
+ self.status = :failed
58
+ self.error = e
59
+ raise
60
+ end
24
61
 
25
- def execute(**changeset)
62
+ def set_mapped_options(acc)
26
63
  mapper = options_mapper || default_options_mapper
27
- mapper_args = [changeset, self].take(mapper.arity)
64
+ mapper_args = [acc, self].take(mapper.arity)
28
65
  self.mapped_options = mapper.call(*mapper_args)
66
+ end
29
67
 
30
- guard_args = [changeset, self]
68
+ def to_s
69
+ @to_s.to_s
70
+ end
31
71
 
32
- if_guard_args = guard_args.take(if_guard.arity)
33
- unless_guard_guard_args = guard_args.take(unless_guard.arity)
72
+ private
34
73
 
35
- if !instrumentalize_callable(unless_guard, unless_guard_guard_args) && instrumentalize_callable(if_guard, if_guard_args)
36
- constructor_args = [self, changeset]
37
- constructor_args = constructor_args.take(constructor.arity)
74
+ attr_writer :result, :error, :mapped_options, :execution_started_at, :execution_finished_at, :execution_duration
75
+ attr_reader :constructor, :options_mapper, :pipeline, :callbacks
38
76
 
39
- self.result = instrumentalize_callable(constructor, constructor_args)
40
- end
77
+ def evaluate_if_guard(args)
78
+ execute_callable(if_guard, args)
79
+ end
41
80
 
42
- set_status
43
- result
44
- rescue StandardError => e
45
- self.status = :failed
46
- self.error = e
47
- raise
81
+ def evaluate_unless_guard(args)
82
+ !execute_callable(unless_guard, args)
48
83
  end
49
84
 
50
- # def type?(potential_type)
51
- # constructor.resolve_type(potential_type)
52
- # end
85
+ def set_result(acc)
86
+ args = [acc, self]
87
+ self.result = execute_callable(constructor, args)
88
+ end
53
89
 
54
- private
90
+ def execute_callable(callable, args)
91
+ args = args.take(callable.arity) unless callable.arity.negative?
55
92
 
56
- attr_writer :result, :status, :error, :mapped_options
57
- attr_reader :constructor, :options_mapper
58
-
59
- def instrumentalize_callable(callable, args)
60
- if args.last.is_a?(Hash)
61
- callable.call(*args.take(args.length - 1), **args.last)
62
- else
63
- callable.call(*args)
64
- end
93
+ callable.call(*args)
65
94
  end
66
95
 
67
96
  def if_guard
@@ -76,8 +105,10 @@ module NxtPipeline
76
105
  -> { result }
77
106
  end
78
107
 
79
- def define_attr_readers(opts)
80
- opts.each do |key, value|
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|
81
112
  define_singleton_method key.to_s do
82
113
  value
83
114
  end
@@ -85,12 +116,46 @@ module NxtPipeline
85
116
  end
86
117
 
87
118
  def set_status
119
+ return if status.present? # We do not set it if the constructor did already
120
+
88
121
  self.status = result.present? ? :success : :skipped
89
122
  end
90
123
 
124
+ def track_execution_time(&block)
125
+ set_execution_started_at
126
+ block.call
127
+ ensure
128
+ set_execution_finished_at
129
+ set_execution_duration
130
+ end
131
+
132
+ def set_execution_started_at
133
+ self.execution_started_at = Time.current
134
+ end
135
+
136
+ def set_execution_finished_at
137
+ self.execution_finished_at = Time.current
138
+ end
139
+
140
+ def set_execution_duration
141
+ self.execution_duration = execution_finished_at - execution_started_at
142
+ end
143
+
91
144
  def default_options_mapper
92
145
  # returns an empty hash
93
- ->(changeset) { {} }
146
+ ->(_) { {} }
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
94
159
  end
95
160
  end
96
161
  end
@@ -1,4 +1,4 @@
1
1
  module NxtPipeline
2
- VERSION = "0.4.2".freeze
2
+ VERSION = "2.0.0".freeze
3
3
  end
4
4
 
data/lib/nxt_pipeline.rb CHANGED
@@ -1,10 +1,49 @@
1
1
  require 'active_support/all'
2
+ require 'nxt_registry'
2
3
  require 'nxt_pipeline/version'
3
4
  require 'nxt_pipeline/logger'
4
- require 'nxt_pipeline/constructor'
5
5
  require 'nxt_pipeline/pipeline'
6
6
  require 'nxt_pipeline/step'
7
+ require 'nxt_pipeline/callbacks'
7
8
  require 'nxt_pipeline/error_callback'
8
9
 
9
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
10
49
  end
data/nxt_pipeline.gemspec CHANGED
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.require_paths = ["lib"]
36
36
 
37
37
  spec.add_dependency "activesupport"
38
+ spec.add_dependency "nxt_registry"
38
39
  spec.add_development_dependency "bundler", "~> 2.1"
39
40
  spec.add_development_dependency "guard-rspec"
40
41
  spec.add_development_dependency "rake", "~> 13.0"
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: 0.4.2
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: 2020-10-13 00:00:00.000000000 Z
13
+ date: 2022-05-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -26,6 +26,20 @@ 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
@@ -96,7 +110,7 @@ dependencies:
96
110
  - - ">="
97
111
  - !ruby/object:Gem::Version
98
112
  version: '0'
99
- description:
113
+ description:
100
114
  email:
101
115
  - mail@nilssommer.de
102
116
  executables: []
@@ -121,7 +135,7 @@ files:
121
135
  - bin/rspec
122
136
  - bin/setup
123
137
  - lib/nxt_pipeline.rb
124
- - lib/nxt_pipeline/constructor.rb
138
+ - lib/nxt_pipeline/callbacks.rb
125
139
  - lib/nxt_pipeline/error_callback.rb
126
140
  - lib/nxt_pipeline/logger.rb
127
141
  - lib/nxt_pipeline/pipeline.rb
@@ -135,7 +149,7 @@ metadata:
135
149
  allowed_push_host: https://rubygems.org
136
150
  homepage_uri: https://github.com/nxt-insurance/nxt_pipeline
137
151
  source_code_uri: https://github.com/nxt-insurance/nxt_pipeline.git
138
- post_install_message:
152
+ post_install_message:
139
153
  rdoc_options: []
140
154
  require_paths:
141
155
  - lib
@@ -151,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
165
  version: '0'
152
166
  requirements: []
153
167
  rubygems_version: 3.1.2
154
- signing_key:
168
+ signing_key:
155
169
  specification_version: 4
156
170
  summary: DSL to build Pipeline with mountable Segments to process things.
157
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