nxt_pipeline 0.4.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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