ruby_reactor 0.7.1 → 0.8.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/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/.specify/memory/constitution.md +26 -16
- data/.specify/templates/plan-template.md +4 -0
- data/.specify/templates/tasks-template.md +1 -1
- data/CHANGELOG.md +140 -0
- data/CLAUDE.md +1 -1
- data/README.md +125 -31
- data/lib/ruby_reactor/context.rb +2 -2
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
- data/lib/ruby_reactor/dsl/reactor.rb +36 -18
- data/lib/ruby_reactor/dsl/step_builder.rb +95 -2
- data/lib/ruby_reactor/dsl/template_helpers.rb +2 -2
- data/lib/ruby_reactor/dsl/validation_helpers.rb +17 -0
- data/lib/ruby_reactor/error/input_validation_error.rb +4 -0
- data/lib/ruby_reactor/error/step_failure_error.rb +10 -3
- data/lib/ruby_reactor/executor/result_handler.rb +9 -3
- data/lib/ruby_reactor/executor/retry_manager.rb +2 -1
- data/lib/ruby_reactor/executor/step_executor.rb +9 -2
- data/lib/ruby_reactor/executor.rb +3 -0
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +3 -2
- data/lib/ruby_reactor/reactor.rb +9 -12
- data/lib/ruby_reactor/rspec/matchers.rb +3 -6
- data/lib/ruby_reactor/step/async_reactor_step.rb +159 -162
- data/lib/ruby_reactor/step/compose_step.rb +56 -75
- data/lib/ruby_reactor/step/input_contract.rb +128 -0
- data/lib/ruby_reactor/step/map_step.rb +177 -218
- data/lib/ruby_reactor/step.rb +116 -21
- data/lib/ruby_reactor/step_signals.rb +6 -2
- data/lib/ruby_reactor/step_worker.rb +25 -10
- data/lib/ruby_reactor/template/result.rb +9 -2
- data/lib/ruby_reactor/utils/fetch_indifferent.rb +13 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor.rb +5 -2
- data/specs/002-step-input-contracts/checklists/requirements.md +49 -0
- data/specs/002-step-input-contracts/contracts/dsl-surface.md +193 -0
- data/specs/002-step-input-contracts/data-model.md +115 -0
- data/specs/002-step-input-contracts/plan.md +165 -0
- data/specs/002-step-input-contracts/quickstart.md +170 -0
- data/specs/002-step-input-contracts/research.md +233 -0
- data/specs/002-step-input-contracts/spec.md +359 -0
- data/specs/002-step-input-contracts/tasks.md +367 -0
- data/specs/004-inheritable-step-class/checklists/requirements.md +40 -0
- data/specs/004-inheritable-step-class/contracts/step-lifecycle.md +85 -0
- data/specs/004-inheritable-step-class/data-model.md +116 -0
- data/specs/004-inheritable-step-class/plan.md +174 -0
- data/specs/004-inheritable-step-class/quickstart.md +112 -0
- data/specs/004-inheritable-step-class/research.md +308 -0
- data/specs/004-inheritable-step-class/spec.md +316 -0
- data/specs/004-inheritable-step-class/tasks.md +258 -0
- data/specs/deferred-003-step-lock-declarations/checklists/requirements.md +51 -0
- data/specs/deferred-003-step-lock-declarations/contracts/dsl-surface.md +154 -0
- data/specs/deferred-003-step-lock-declarations/data-model.md +131 -0
- data/specs/deferred-003-step-lock-declarations/plan.md +166 -0
- data/specs/deferred-003-step-lock-declarations/quickstart.md +169 -0
- data/specs/deferred-003-step-lock-declarations/research.md +196 -0
- data/specs/deferred-003-step-lock-declarations/spec.md +447 -0
- data/specs/deferred-003-step-lock-declarations/tasks.md +572 -0
- data/specs/possible_feature.md +22 -0
- metadata +28 -1
|
@@ -25,17 +25,38 @@ module RubyReactor
|
|
|
25
25
|
@args_validator = nil
|
|
26
26
|
@output_validator = nil
|
|
27
27
|
@retry_config = {}
|
|
28
|
+
@inline_contract = nil
|
|
29
|
+
@rule_sites = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Deprecation notices print once per declaration site for the process.
|
|
33
|
+
def self.deprecation_sites
|
|
34
|
+
@deprecation_sites ||= Set.new
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
def argument(name, source, type = nil, transform: nil, **predicates)
|
|
31
38
|
@arguments[name] = {
|
|
32
39
|
source: source,
|
|
33
|
-
transform: transform
|
|
40
|
+
transform: transform,
|
|
41
|
+
origin: :explicit
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
return unless type || predicates.any?
|
|
37
45
|
|
|
38
46
|
@arg_validations << [name, type, false, predicates]
|
|
47
|
+
@rule_sites << [name, caller_locations(1, 1).first]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# An inline step's input contract. Inside the block `input` declares, as
|
|
51
|
+
# in a step class; outside it `input(:x)` stays the template reference.
|
|
52
|
+
def inputs(&block)
|
|
53
|
+
if @impl
|
|
54
|
+
raise Error::ValidationError,
|
|
55
|
+
"step :#{@name}: `inputs` is for inline steps; declare `input` inside #{@impl} instead."
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@inline_contract ||= RubyReactor::Step::InputContract.new(owner: @name)
|
|
59
|
+
@inline_contract.instance_eval(&block)
|
|
39
60
|
end
|
|
40
61
|
|
|
41
62
|
def run(&block)
|
|
@@ -67,6 +88,7 @@ module RubyReactor
|
|
|
67
88
|
# pre-built schema) is applied last and wins on conflicts.
|
|
68
89
|
def validate_args(schema_or_validator = nil, &block)
|
|
69
90
|
@validate_args_input = block || schema_or_validator
|
|
91
|
+
@validate_args_site = caller_locations(1, 1).first
|
|
70
92
|
end
|
|
71
93
|
|
|
72
94
|
# Scalar-aware output validation.
|
|
@@ -114,6 +136,9 @@ module RubyReactor
|
|
|
114
136
|
# unit rather than run inline — `:step` for `async_step`, `:reactor` for
|
|
115
137
|
# `async_reactor`. Nil for an ordinary step.
|
|
116
138
|
def build(async_dispatch: nil)
|
|
139
|
+
check_contract_conflicts!
|
|
140
|
+
warn_deprecated_rules
|
|
141
|
+
|
|
117
142
|
step_config = {
|
|
118
143
|
async_dispatch: async_dispatch,
|
|
119
144
|
name: @name,
|
|
@@ -127,16 +152,78 @@ module RubyReactor
|
|
|
127
152
|
dependencies: @dependencies,
|
|
128
153
|
args_validator: @args_validator || build_args_validator(@arg_validations, @validate_args_input),
|
|
129
154
|
output_validator: @output_validator,
|
|
155
|
+
inline_contract: @inline_contract,
|
|
130
156
|
retry_config: @retry_config.empty? ? (@reactor&.retry_defaults || {}) : @retry_config
|
|
131
157
|
}
|
|
132
158
|
|
|
133
159
|
RubyReactor::Dsl::StepConfig.new(step_config)
|
|
134
160
|
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# A step that owns its input contract takes wiring only from the
|
|
165
|
+
# reactor: rules here would be a second, overlapping rule set.
|
|
166
|
+
def check_contract_conflicts!
|
|
167
|
+
contract = owned_contract
|
|
168
|
+
return unless contract
|
|
169
|
+
|
|
170
|
+
owner = @impl || "its `inputs do ... end` block"
|
|
171
|
+
if (arg = @arg_validations.first&.first)
|
|
172
|
+
raise Error::ValidationError,
|
|
173
|
+
"#{reactor_label} step :#{@name} declares rules on argument :#{arg}, but #{owner} owns its input " \
|
|
174
|
+
"contract. Move the rule into #{owner} (`input :#{arg}, ...`) and keep only the wiring here: " \
|
|
175
|
+
"`argument :#{arg}, <source>`."
|
|
176
|
+
end
|
|
177
|
+
if @validate_args_input
|
|
178
|
+
raise Error::ValidationError,
|
|
179
|
+
"#{reactor_label} step :#{@name} declares `validate_args`, but #{owner} owns its input contract. " \
|
|
180
|
+
"Move the rule into #{owner} (`validate_inputs ...`) and keep only the wiring here."
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
unknown = @arguments.keys.find { |name| !contract.declares?(name) }
|
|
184
|
+
return unless unknown
|
|
185
|
+
|
|
186
|
+
raise Error::ValidationError,
|
|
187
|
+
"#{reactor_label} step :#{@name} wires argument :#{unknown}, which #{owner} does not declare. " \
|
|
188
|
+
"Declared inputs: #{contract.declarations.keys.join(", ")}."
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Rules on `argument` / `validate_args` still work for a step without a
|
|
192
|
+
# contract; they now belong on the step itself.
|
|
193
|
+
def warn_deprecated_rules
|
|
194
|
+
target = @impl || "an `inputs do ... end` block"
|
|
195
|
+
@rule_sites.each do |arg, site|
|
|
196
|
+
warn_deprecation(site, "step :#{@name} declares rules on `argument :#{arg}`. Declare them on the step " \
|
|
197
|
+
"instead (`input :#{arg}, ...` in #{target}) and keep `argument :#{arg}, <source>` " \
|
|
198
|
+
"for wiring.")
|
|
199
|
+
end
|
|
200
|
+
return unless @validate_args_site
|
|
201
|
+
|
|
202
|
+
warn_deprecation(@validate_args_site, "step :#{@name} declares rules with `validate_args`. Declare them on " \
|
|
203
|
+
"the step instead (`validate_inputs` in #{target}).")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def warn_deprecation(site, message)
|
|
207
|
+
location = "#{site.path}:#{site.lineno}"
|
|
208
|
+
return unless StepBuilder.deprecation_sites.add?(location)
|
|
209
|
+
|
|
210
|
+
warn "[RubyReactor] DEPRECATION: #{location} #{reactor_label} #{message} " \
|
|
211
|
+
"Removal no earlier than the next MAJOR."
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def owned_contract
|
|
215
|
+
@inline_contract || (@impl.input_contract if @impl.respond_to?(:declares_inputs?) && @impl.declares_inputs?)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def reactor_label
|
|
219
|
+
@reactor&.name || @reactor.inspect
|
|
220
|
+
end
|
|
135
221
|
end
|
|
136
222
|
|
|
137
223
|
class StepConfig
|
|
138
224
|
attr_reader :name, :impl, :arguments, :run_block, :compensate_block, :undo_block, :conditions, :guards,
|
|
139
|
-
:dependencies, :args_validator, :output_validator, :retry_config, :async_dispatch
|
|
225
|
+
:dependencies, :args_validator, :output_validator, :retry_config, :async_dispatch,
|
|
226
|
+
:inline_contract
|
|
140
227
|
|
|
141
228
|
def initialize(config)
|
|
142
229
|
@async_dispatch = config[:async_dispatch]
|
|
@@ -151,6 +238,7 @@ module RubyReactor
|
|
|
151
238
|
@dependencies = config[:dependencies] || []
|
|
152
239
|
@args_validator = config[:args_validator]
|
|
153
240
|
@output_validator = config[:output_validator]
|
|
241
|
+
@inline_contract = config[:inline_contract]
|
|
154
242
|
@retry_config = { max_attempts: 1 }.merge(config[:retry_config] || {})
|
|
155
243
|
end
|
|
156
244
|
|
|
@@ -165,6 +253,11 @@ module RubyReactor
|
|
|
165
253
|
!@impl.nil?
|
|
166
254
|
end
|
|
167
255
|
|
|
256
|
+
# The contract that governs this step's inputs, or nil when it has none.
|
|
257
|
+
def input_contract
|
|
258
|
+
@inline_contract || (@impl.input_contract if @impl.respond_to?(:declares_inputs?) && @impl.declares_inputs?)
|
|
259
|
+
end
|
|
260
|
+
|
|
168
261
|
def has_run_block?
|
|
169
262
|
!@run_block.nil?
|
|
170
263
|
end
|
|
@@ -29,9 +29,9 @@ module RubyReactor
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# rubocop:disable Naming/MethodName
|
|
32
|
-
def Failure(
|
|
32
|
+
def Failure(...)
|
|
33
33
|
# rubocop:enable Naming/MethodName
|
|
34
|
-
RubyReactor.Failure(
|
|
34
|
+
RubyReactor.Failure(...)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# rubocop:disable Naming/MethodName
|
|
@@ -36,6 +36,23 @@ module RubyReactor
|
|
|
36
36
|
RubyReactor::Validation::InputValidator.new(schema)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Dispatch across the layered `input` forms, shared by reactor inputs and
|
|
40
|
+
# step input contracts:
|
|
41
|
+
# Form 3 — pre-built schema / contract (`validate:`)
|
|
42
|
+
# Form 2 — block bound to the value macro (`do |i| ... end`)
|
|
43
|
+
# legacy — single-key schema block (`do required(:name)... end`)
|
|
44
|
+
# Form 1 / 1b — inline scalar or class type
|
|
45
|
+
# Form 0 — declaration only (no validator)
|
|
46
|
+
def build_declaration_validator(name, type, optional, validate, predicates, &block)
|
|
47
|
+
if validate
|
|
48
|
+
create_input_validator(validate)
|
|
49
|
+
elsif block
|
|
50
|
+
block.arity.nonzero? ? build_macro_validator(name, optional, &block) : create_input_validator(block)
|
|
51
|
+
elsif type || predicates.any?
|
|
52
|
+
build_inline_validator(name, type, optional, predicates)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
39
56
|
# Compose per-argument inline rules with an optional `validate_args`
|
|
40
57
|
# block / pre-built schema. Returns nil when there is nothing to validate.
|
|
41
58
|
def build_args_validator(inline_rules, validate_input)
|
|
@@ -3,18 +3,25 @@
|
|
|
3
3
|
module RubyReactor
|
|
4
4
|
module Error
|
|
5
5
|
class StepFailureError < Base
|
|
6
|
-
attr_reader :step_arguments, :exception_class
|
|
6
|
+
attr_reader :step_arguments, :exception_class, :validation_errors
|
|
7
7
|
|
|
8
8
|
# rubocop:disable Metrics/ParameterLists
|
|
9
|
-
def initialize(message, step: nil, context: nil, original_error: nil, step_arguments: {}, exception_class: nil
|
|
9
|
+
def initialize(message, step: nil, context: nil, original_error: nil, step_arguments: {}, exception_class: nil,
|
|
10
|
+
validation_errors: nil)
|
|
10
11
|
# rubocop:enable Metrics/ParameterLists
|
|
11
12
|
super(message, step: step, context: context, original_error: original_error)
|
|
12
13
|
@step_arguments = step_arguments
|
|
13
14
|
@exception_class = exception_class
|
|
15
|
+
@validation_errors = validation_errors
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
# Defers to the original error when it has an opinion (e.g. a step's own
|
|
19
|
+
# `Error::InputValidationError`, always non-retryable) so that opinion
|
|
20
|
+
# survives being wrapped into a StepFailureError — the same
|
|
21
|
+
# `respond_to?(:retryable?)` protocol `RubyReactor::Failure` itself uses.
|
|
22
|
+
# Defaults to `true` for an ordinary step failure, unchanged from before.
|
|
16
23
|
def retryable?
|
|
17
|
-
true
|
|
24
|
+
original_error.respond_to?(:retryable?) ? original_error.retryable? : true
|
|
18
25
|
end
|
|
19
26
|
end
|
|
20
27
|
end
|
|
@@ -143,7 +143,8 @@ module RubyReactor
|
|
|
143
143
|
orig_err = result.original_error.is_a?(Exception) ? result.original_error : nil
|
|
144
144
|
error = Error::StepFailureError.new(result.error, step: step_config.name, context: @context,
|
|
145
145
|
original_error: orig_err,
|
|
146
|
-
step_arguments: resolved_arguments
|
|
146
|
+
step_arguments: resolved_arguments,
|
|
147
|
+
validation_errors: result.validation_errors)
|
|
147
148
|
if result.respond_to?(:backtrace) && result.backtrace
|
|
148
149
|
error.set_backtrace(result.backtrace)
|
|
149
150
|
elsif orig_err
|
|
@@ -155,9 +156,12 @@ module RubyReactor
|
|
|
155
156
|
def handle_failure(step_config, result, resolved_arguments)
|
|
156
157
|
failure_result = @compensation_manager.handle_step_failure(step_config, result.error, resolved_arguments)
|
|
157
158
|
orig_err = result.error.is_a?(Exception) ? result.error : nil
|
|
159
|
+
# A step that propagates another unit's validation failure (an
|
|
160
|
+
# async_step reader) keeps its field errors on the reactor's failure.
|
|
158
161
|
error = Error::StepFailureError.new(failure_result.error, step: step_config.name, context: @context,
|
|
159
162
|
original_error: orig_err,
|
|
160
|
-
step_arguments: resolved_arguments
|
|
163
|
+
step_arguments: resolved_arguments,
|
|
164
|
+
validation_errors: result.validation_errors)
|
|
161
165
|
if result.respond_to?(:backtrace) && result.backtrace
|
|
162
166
|
error.set_backtrace(result.backtrace)
|
|
163
167
|
elsif orig_err
|
|
@@ -222,7 +226,9 @@ module RubyReactor
|
|
|
222
226
|
exception_class: exception_class,
|
|
223
227
|
file_path: file_path,
|
|
224
228
|
line_number: line_number,
|
|
225
|
-
code_snippet: code_snippet
|
|
229
|
+
code_snippet: code_snippet,
|
|
230
|
+
validation_errors: error.validation_errors,
|
|
231
|
+
retryable: error.retryable?
|
|
226
232
|
)
|
|
227
233
|
end
|
|
228
234
|
|
|
@@ -185,7 +185,8 @@ module RubyReactor
|
|
|
185
185
|
[]
|
|
186
186
|
end,
|
|
187
187
|
reactor_name: reactor_class.name,
|
|
188
|
-
step_arguments: result.respond_to?(:step_arguments) ? result.step_arguments : {}
|
|
188
|
+
step_arguments: result.respond_to?(:step_arguments) ? result.step_arguments : {},
|
|
189
|
+
validation_errors: result.validation_errors
|
|
189
190
|
)
|
|
190
191
|
end
|
|
191
192
|
|
|
@@ -183,9 +183,13 @@ module RubyReactor
|
|
|
183
183
|
def safe_execute_step_sync(step_config, resolved_arguments = nil)
|
|
184
184
|
resolved_arguments ||= resolve_arguments(step_config)
|
|
185
185
|
execute_step_sync_without_result_handling(step_config, resolved_arguments)
|
|
186
|
-
rescue Error::InputValidationError
|
|
186
|
+
rescue Error::InputValidationError => e
|
|
187
187
|
# Validation failures are not retryable and must surface as a structured
|
|
188
188
|
# InputValidationError (with field_errors), so let them propagate.
|
|
189
|
+
# A step class stamps its own class name; inside a reactor the step's
|
|
190
|
+
# name there is the useful attribution, so it overwrites.
|
|
191
|
+
e.step_name = step_config.name
|
|
192
|
+
e.step_arguments ||= resolved_arguments
|
|
189
193
|
raise
|
|
190
194
|
rescue StandardError => e
|
|
191
195
|
# Identify redacted inputs
|
|
@@ -340,13 +344,16 @@ module RubyReactor
|
|
|
340
344
|
end
|
|
341
345
|
|
|
342
346
|
def run_step_implementation(step_config, arguments)
|
|
347
|
+
contract = step_config.input_contract
|
|
343
348
|
@context.append_execution_trace(
|
|
344
|
-
{ type: :run, step: step_config.name, timestamp: Time.now,
|
|
349
|
+
{ type: :run, step: step_config.name, timestamp: Time.now,
|
|
350
|
+
arguments: contract ? contract.redact(arguments) : arguments }
|
|
345
351
|
)
|
|
346
352
|
if step_config.has_run_block?
|
|
347
353
|
# Execute inline block
|
|
348
354
|
# If no arguments are defined for the step, pass the reactor inputs as arguments
|
|
349
355
|
args_to_pass = arguments.empty? ? @context.inputs : arguments
|
|
356
|
+
args_to_pass = step_config.inline_contract.enforce!(args_to_pass) if step_config.inline_contract
|
|
350
357
|
catch(StepSignals::TAG) { step_config.run_block.call(args_to_pass, @context) }
|
|
351
358
|
elsif step_config.has_impl?
|
|
352
359
|
# Execute step class
|
|
@@ -19,6 +19,9 @@ module RubyReactor
|
|
|
19
19
|
:step_executor, :result, :middlewares
|
|
20
20
|
|
|
21
21
|
def initialize(reactor_class, inputs = {}, context = nil)
|
|
22
|
+
# Resume, map, compose and background workers build an Executor without
|
|
23
|
+
# going through Reactor#run; the inferred wiring must exist there too.
|
|
24
|
+
reactor_class.validate_definition! if reactor_class.respond_to?(:validate_definition!)
|
|
22
25
|
@reactor_class = reactor_class
|
|
23
26
|
@context = context || Context.new(inputs, reactor_class)
|
|
24
27
|
@middlewares = Executor.middlewares_for(reactor_class)
|
|
@@ -7,11 +7,12 @@ module RubyReactor
|
|
|
7
7
|
# rubocop:disable Metrics/ParameterLists
|
|
8
8
|
def initialize(message, step:, attempts:, original_error: nil,
|
|
9
9
|
inputs: {}, backtrace: nil, redact_inputs: [],
|
|
10
|
-
reactor_name: nil, step_arguments: {})
|
|
10
|
+
reactor_name: nil, step_arguments: {}, validation_errors: nil)
|
|
11
11
|
# rubocop:enable Metrics/ParameterLists
|
|
12
12
|
super(message,
|
|
13
13
|
step_name: step, inputs: inputs, backtrace: backtrace,
|
|
14
|
-
redact_inputs: redact_inputs, reactor_name: reactor_name, step_arguments: step_arguments
|
|
14
|
+
redact_inputs: redact_inputs, reactor_name: reactor_name, step_arguments: step_arguments,
|
|
15
|
+
validation_errors: validation_errors)
|
|
15
16
|
@attempts = attempts
|
|
16
17
|
@original_error = original_error
|
|
17
18
|
end
|
data/lib/ruby_reactor/reactor.rb
CHANGED
|
@@ -84,21 +84,17 @@ module RubyReactor
|
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
# rubocop:disable Metrics/MethodLength
|
|
88
87
|
def run(inputs = {})
|
|
88
|
+
# Before the context exists, so an incomplete definition never saves one.
|
|
89
|
+
self.class.validate_definition!
|
|
90
|
+
|
|
89
91
|
# For all reactors, initialize context first to capture execution ID
|
|
90
92
|
@context = @context.is_a?(Context) ? @context : Context.new(inputs, self.class)
|
|
91
93
|
|
|
92
94
|
# Validate inputs
|
|
93
95
|
validation_result = self.class.validate_inputs(inputs)
|
|
94
96
|
if validation_result.failure?
|
|
95
|
-
|
|
96
|
-
@context.status = "failed"
|
|
97
|
-
@context.failure_reason = {
|
|
98
|
-
message: validation_result.error.message,
|
|
99
|
-
validation_errors: validation_result.error.field_errors
|
|
100
|
-
}
|
|
101
|
-
save_context
|
|
97
|
+
handle_validation_failure(validation_result)
|
|
102
98
|
return validation_result
|
|
103
99
|
end
|
|
104
100
|
|
|
@@ -144,7 +140,6 @@ module RubyReactor
|
|
|
144
140
|
end
|
|
145
141
|
@result
|
|
146
142
|
end
|
|
147
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
148
143
|
|
|
149
144
|
def continue(payload:, step_name:, idempotency_key: nil)
|
|
150
145
|
_ = idempotency_key
|
|
@@ -270,8 +265,9 @@ module RubyReactor
|
|
|
270
265
|
reason = @context.failure_reason || {}
|
|
271
266
|
return reason if reason.is_a?(RubyReactor::Failure)
|
|
272
267
|
|
|
273
|
-
#
|
|
274
|
-
|
|
268
|
+
# Presence-aware: a stored `retryable: false` must not be swallowed by an
|
|
269
|
+
# `||` fallback and silently default back to retryable.
|
|
270
|
+
r = ->(k) { Utils::FetchIndifferent.call(reason, k) }
|
|
275
271
|
|
|
276
272
|
Failure.new(
|
|
277
273
|
r[:message],
|
|
@@ -314,7 +310,8 @@ module RubyReactor
|
|
|
314
310
|
@context.status = "failed"
|
|
315
311
|
@context.failure_reason = {
|
|
316
312
|
message: result.error.message,
|
|
317
|
-
validation_errors: result.error.field_errors
|
|
313
|
+
validation_errors: result.error.field_errors,
|
|
314
|
+
retryable: result.retryable?
|
|
318
315
|
}
|
|
319
316
|
save_context
|
|
320
317
|
end
|
|
@@ -156,12 +156,9 @@ module RubyReactor
|
|
|
156
156
|
subject.ensure_executed!
|
|
157
157
|
return false unless subject.failure?
|
|
158
158
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
# If failure is InputValidationError, it might be serialized differently
|
|
163
|
-
# Or stored in validation_errors key
|
|
164
|
-
errors = reason["validation_errors"] || reason[:validation_errors]
|
|
159
|
+
# `result` normalizes both failure shapes — a reactor-input failure
|
|
160
|
+
# stored as a hash, and a step failure stored as a Failure.
|
|
161
|
+
errors = subject.result.validation_errors
|
|
165
162
|
|
|
166
163
|
if errors
|
|
167
164
|
errors.key?(field.to_s) || errors.key?(field.to_sym)
|