ruby_reactor 0.3.0 → 0.3.1
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/README.md +31 -4
- data/documentation/testing.md +812 -0
- data/lib/ruby_reactor/configuration.rb +1 -1
- data/lib/ruby_reactor/context.rb +13 -5
- data/lib/ruby_reactor/context_serializer.rb +55 -4
- data/lib/ruby_reactor/dsl/reactor.rb +3 -2
- data/lib/ruby_reactor/error/step_failure_error.rb +5 -2
- data/lib/ruby_reactor/executor/result_handler.rb +8 -2
- data/lib/ruby_reactor/executor/retry_manager.rb +15 -7
- data/lib/ruby_reactor/executor/step_executor.rb +24 -99
- data/lib/ruby_reactor/executor.rb +3 -13
- data/lib/ruby_reactor/map/collector.rb +16 -15
- data/lib/ruby_reactor/map/element_executor.rb +90 -104
- data/lib/ruby_reactor/map/execution.rb +2 -1
- data/lib/ruby_reactor/map/helpers.rb +2 -1
- data/lib/ruby_reactor/map/result_enumerator.rb +1 -1
- data/lib/ruby_reactor/reactor.rb +174 -16
- data/lib/ruby_reactor/rspec/helpers.rb +17 -0
- data/lib/ruby_reactor/rspec/matchers.rb +256 -0
- data/lib/ruby_reactor/rspec/step_executor_patch.rb +85 -0
- data/lib/ruby_reactor/rspec/test_subject.rb +625 -0
- data/lib/ruby_reactor/rspec.rb +18 -0
- data/lib/ruby_reactor/{async_router.rb → sidekiq_adapter.rb} +10 -5
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +1 -3
- data/lib/ruby_reactor/step/compose_step.rb +0 -1
- data/lib/ruby_reactor/step/map_step.rb +11 -18
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +32 -24
- data/lib/ruby_reactor.rb +70 -10
- metadata +9 -3
|
@@ -84,7 +84,6 @@ module RubyReactor
|
|
|
84
84
|
def link_contexts(child_context, parent_context)
|
|
85
85
|
child_context.parent_context = parent_context
|
|
86
86
|
child_context.root_context = parent_context.root_context || parent_context
|
|
87
|
-
child_context.test_mode = parent_context.test_mode
|
|
88
87
|
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
89
88
|
end
|
|
90
89
|
|
|
@@ -57,7 +57,9 @@ module RubyReactor
|
|
|
57
57
|
private
|
|
58
58
|
|
|
59
59
|
def should_run_async?(arguments, context)
|
|
60
|
-
|
|
60
|
+
return false if context.inline_async_execution
|
|
61
|
+
|
|
62
|
+
arguments[:async]
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
def run_inline(arguments, context)
|
|
@@ -118,30 +120,21 @@ module RubyReactor
|
|
|
118
120
|
def link_contexts(child_context, parent_context)
|
|
119
121
|
child_context.parent_context = parent_context
|
|
120
122
|
child_context.root_context = parent_context.root_context || parent_context
|
|
121
|
-
child_context.test_mode = parent_context.test_mode
|
|
122
123
|
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
123
124
|
end
|
|
124
125
|
|
|
125
|
-
def process_results(results, collect_block,
|
|
126
|
+
def process_results(results, collect_block, _fail_fast = true)
|
|
126
127
|
if collect_block
|
|
127
128
|
begin
|
|
128
129
|
# Collect block receives Result objects when fail_fast is false, values when true
|
|
129
|
-
RubyReactor::Success(collect_block.call(results))
|
|
130
|
+
return RubyReactor::Success(collect_block.call(results))
|
|
130
131
|
rescue StandardError => e
|
|
131
|
-
RubyReactor::Failure(e)
|
|
132
|
+
return RubyReactor::Failure(e)
|
|
132
133
|
end
|
|
133
|
-
elsif fail_fast
|
|
134
|
-
# Default behavior when no collect block
|
|
135
|
-
# Current behavior: results are already values
|
|
136
|
-
RubyReactor::Success(results)
|
|
137
|
-
else
|
|
138
|
-
# New behavior: extract successful values only
|
|
139
|
-
# New behavior: extract successful values only IF fail_fast is true behavior implies only values
|
|
140
|
-
# However, if fail_fast is false, we want to return results as is, or if logic dictates otherwise.
|
|
141
|
-
# wait, if fail_fast=false, we expect Result objects so we can check if success/failure.
|
|
142
|
-
# If we return only successes, we hide failures.
|
|
143
|
-
RubyReactor::Success(results)
|
|
144
134
|
end
|
|
135
|
+
|
|
136
|
+
# Simplified: both branches returned Success(results)
|
|
137
|
+
RubyReactor::Success(results)
|
|
145
138
|
end
|
|
146
139
|
|
|
147
140
|
def extract_path(value, path)
|
|
@@ -203,7 +196,7 @@ module RubyReactor
|
|
|
203
196
|
argument_mappings: arguments[:argument_mappings],
|
|
204
197
|
strict_ordering: arguments[:strict_ordering],
|
|
205
198
|
mapped_reactor_class: arguments[:mapped_reactor_class],
|
|
206
|
-
fail_fast: arguments[:fail_fast]
|
|
199
|
+
fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
|
|
207
200
|
)
|
|
208
201
|
queue_collector(map_id, context, step_name, arguments[:strict_ordering])
|
|
209
202
|
"map:#{map_id}"
|
|
@@ -284,7 +277,7 @@ module RubyReactor
|
|
|
284
277
|
map_id: map_id, serialized_inputs: serialized_inputs,
|
|
285
278
|
reactor_class_info: reactor_class_info, strict_ordering: arguments[:strict_ordering],
|
|
286
279
|
parent_context_id: context.context_id, parent_reactor_class_name: context.reactor_class.name,
|
|
287
|
-
step_name: step_name.to_s, fail_fast: arguments[:fail_fast]
|
|
280
|
+
step_name: step_name.to_s, fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
|
|
288
281
|
)
|
|
289
282
|
end
|
|
290
283
|
end
|
data/lib/ruby_reactor/version.rb
CHANGED
data/lib/ruby_reactor/web/api.rb
CHANGED
|
@@ -24,39 +24,44 @@ module RubyReactor
|
|
|
24
24
|
r.on String do |reactor_id|
|
|
25
25
|
# GET /api/reactors/:id
|
|
26
26
|
r.get do
|
|
27
|
-
|
|
28
|
-
return { error: "Reactor not found" } unless
|
|
27
|
+
raw_data = RubyReactor::Configuration.instance.storage_adapter.find_context_by_id(reactor_id)
|
|
28
|
+
return { error: "Reactor not found" } unless raw_data
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
# Clean data for API usage
|
|
31
|
+
data = ContextSerializer.deserialize_value(raw_data)
|
|
32
|
+
|
|
33
|
+
reactor_class = data[:reactor_class] ? Object.const_get(data[:reactor_class].to_s) : nil
|
|
31
34
|
structure = {}
|
|
32
35
|
|
|
33
36
|
structure = self.class.build_structure(reactor_class) if reactor_class.respond_to?(:steps)
|
|
34
37
|
|
|
35
|
-
{
|
|
36
|
-
id: data[
|
|
37
|
-
class: data[
|
|
38
|
-
status: if %w[failed paused completed running].include?(data[
|
|
39
|
-
data[
|
|
40
|
-
elsif data[
|
|
38
|
+
response_data = {
|
|
39
|
+
id: data[:context_id],
|
|
40
|
+
class: data[:reactor_class].to_s,
|
|
41
|
+
status: if %w[failed paused completed running].include?(data[:status].to_s)
|
|
42
|
+
data[:status].to_s
|
|
43
|
+
elsif data[:cancelled]
|
|
41
44
|
"cancelled"
|
|
42
45
|
else
|
|
43
|
-
(data[
|
|
46
|
+
(data[:current_step] ? "running" : "completed")
|
|
44
47
|
end,
|
|
45
|
-
current_step: data[
|
|
46
|
-
retry_count: data[
|
|
47
|
-
undo_stack: data[
|
|
48
|
-
step_attempts: data.dig(
|
|
49
|
-
created_at: data[
|
|
50
|
-
inputs: data[
|
|
51
|
-
intermediate_results: data[
|
|
48
|
+
current_step: data[:current_step].to_s,
|
|
49
|
+
retry_count: data[:retry_count] || 0,
|
|
50
|
+
undo_stack: data[:undo_stack] || [],
|
|
51
|
+
step_attempts: data.dig(:retry_context, :step_attempts) || {},
|
|
52
|
+
created_at: data[:started_at],
|
|
53
|
+
inputs: data[:inputs],
|
|
54
|
+
intermediate_results: data[:intermediate_results],
|
|
52
55
|
structure: structure,
|
|
53
|
-
steps: data[
|
|
56
|
+
steps: data[:execution_trace] || [],
|
|
54
57
|
composed_contexts: self.class.hydrate_composed_contexts(
|
|
55
|
-
data[
|
|
56
|
-
data[
|
|
58
|
+
data[:composed_contexts] || {},
|
|
59
|
+
data[:reactor_class]&.to_s
|
|
57
60
|
),
|
|
58
|
-
error: data[
|
|
61
|
+
error: data[:failure_reason]
|
|
59
62
|
}
|
|
63
|
+
|
|
64
|
+
ContextSerializer.simplify_for_api(response_data)
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
# POST /api/reactors/:id/retry
|
|
@@ -159,7 +164,8 @@ module RubyReactor
|
|
|
159
164
|
return {} unless composed_contexts.is_a?(Hash)
|
|
160
165
|
|
|
161
166
|
composed_contexts.transform_values do |value|
|
|
162
|
-
|
|
167
|
+
type = value[:type] || value["type"]
|
|
168
|
+
if ["map_ref", :map_ref].include?(type)
|
|
163
169
|
hydrate_map_ref(value, reactor_class_name)
|
|
164
170
|
else
|
|
165
171
|
value
|
|
@@ -169,10 +175,12 @@ module RubyReactor
|
|
|
169
175
|
|
|
170
176
|
def self.hydrate_map_ref(ref_data, reactor_class_name)
|
|
171
177
|
storage = RubyReactor.configuration.storage_adapter
|
|
172
|
-
map_id = ref_data["map_id"]
|
|
178
|
+
map_id = ref_data[:map_id] || ref_data["map_id"]
|
|
173
179
|
|
|
174
180
|
# Use the specific element reactor class if available, otherwise fallback to parent
|
|
175
|
-
target_reactor_class = ref_data[
|
|
181
|
+
target_reactor_class = ref_data[:element_reactor_class] ||
|
|
182
|
+
ref_data["element_reactor_class"] ||
|
|
183
|
+
reactor_class_name
|
|
176
184
|
|
|
177
185
|
# 1. Check for specific failure (O(1))
|
|
178
186
|
# Stored by ResultHandler when a map element fails
|
data/lib/ruby_reactor.rb
CHANGED
|
@@ -20,7 +20,7 @@ rescue LoadError
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
loader = Zeitwerk::Loader.for_gem
|
|
23
|
-
loader.inflector.inflect("api" => "API")
|
|
23
|
+
loader.inflector.inflect("api" => "API", "rspec" => "RSpec")
|
|
24
24
|
loader.setup
|
|
25
25
|
|
|
26
26
|
module RubyReactor
|
|
@@ -39,20 +39,42 @@ module RubyReactor
|
|
|
39
39
|
def failure?
|
|
40
40
|
false
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
{ success: true, value: @value }
|
|
45
|
+
end
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
class Failure
|
|
45
49
|
attr_reader :error, :retryable, :step_name, :inputs, :backtrace, :reactor_name, :step_arguments, :exception_class,
|
|
46
50
|
:file_path, :line_number, :code_snippet, :validation_errors
|
|
47
51
|
|
|
48
|
-
# rubocop:disable Metrics/ParameterLists
|
|
52
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
49
53
|
def initialize(error, retryable: nil, step_name: nil, inputs: {}, backtrace: nil, redact_inputs: [],
|
|
50
54
|
reactor_name: nil, step_arguments: {}, exception_class: nil,
|
|
51
55
|
file_path: nil, line_number: nil, code_snippet: nil, invalid_payload: false, validation_errors: nil)
|
|
52
|
-
# rubocop:enable Metrics/ParameterLists
|
|
56
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
53
57
|
@error = error
|
|
58
|
+
|
|
59
|
+
# Handle case where error is a serialized hash (e.g. from async failure propagation)
|
|
60
|
+
if @error.is_a?(Hash)
|
|
61
|
+
attributes = extract_attributes_from_hash(@error)
|
|
62
|
+
@error = attributes[:error]
|
|
63
|
+
retryable = attributes[:retryable] if retryable.nil?
|
|
64
|
+
step_name ||= attributes[:step_name]
|
|
65
|
+
reactor_name ||= attributes[:reactor_name]
|
|
66
|
+
inputs = attributes[:inputs] if inputs.empty?
|
|
67
|
+
step_arguments = attributes[:step_arguments] if step_arguments.empty?
|
|
68
|
+
raw_backtrace ||= attributes[:backtrace] || backtrace
|
|
69
|
+
exception_class ||= attributes[:exception_class]
|
|
70
|
+
file_path ||= attributes[:file_path]
|
|
71
|
+
line_number ||= attributes[:line_number]
|
|
72
|
+
code_snippet ||= attributes[:code_snippet]
|
|
73
|
+
validation_errors ||= attributes[:validation_errors]
|
|
74
|
+
end
|
|
75
|
+
|
|
54
76
|
@retryable = if retryable.nil?
|
|
55
|
-
error.respond_to?(:retryable?) ? error.retryable? : true
|
|
77
|
+
@error.respond_to?(:retryable?) ? @error.retryable? : true
|
|
56
78
|
else
|
|
57
79
|
retryable
|
|
58
80
|
end
|
|
@@ -60,10 +82,10 @@ module RubyReactor
|
|
|
60
82
|
@reactor_name = reactor_name
|
|
61
83
|
@inputs = inputs
|
|
62
84
|
@step_arguments = step_arguments
|
|
63
|
-
raw_backtrace
|
|
85
|
+
raw_backtrace ||= backtrace || (@error.respond_to?(:backtrace) ? @error.backtrace : caller)
|
|
64
86
|
@backtrace = filter_backtrace(raw_backtrace)
|
|
65
87
|
@redact_inputs = redact_inputs
|
|
66
|
-
@exception_class = exception_class || (error.is_a?(Exception) ? error.class.name : nil)
|
|
88
|
+
@exception_class = exception_class || (@error.is_a?(Exception) ? @error.class.name : nil)
|
|
67
89
|
@file_path = file_path
|
|
68
90
|
@line_number = line_number
|
|
69
91
|
@code_snippet = code_snippet
|
|
@@ -99,6 +121,28 @@ module RubyReactor
|
|
|
99
121
|
msg.join("\n")
|
|
100
122
|
end
|
|
101
123
|
|
|
124
|
+
def to_s
|
|
125
|
+
message
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def to_h
|
|
129
|
+
{
|
|
130
|
+
success: false,
|
|
131
|
+
error: error_message,
|
|
132
|
+
step_name: @step_name,
|
|
133
|
+
inputs: @inputs,
|
|
134
|
+
redact_inputs: @redact_inputs,
|
|
135
|
+
reactor_name: @reactor_name,
|
|
136
|
+
step_arguments: @step_arguments,
|
|
137
|
+
exception_class: @exception_class,
|
|
138
|
+
file_path: @file_path,
|
|
139
|
+
line_number: @line_number,
|
|
140
|
+
code_snippet: @code_snippet,
|
|
141
|
+
validation_errors: @validation_errors,
|
|
142
|
+
backtrace: @backtrace
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
102
146
|
private
|
|
103
147
|
|
|
104
148
|
def build_header
|
|
@@ -147,10 +191,6 @@ module RubyReactor
|
|
|
147
191
|
msg << backtrace.take(10).map { |line| " #{line}" }.join("\n")
|
|
148
192
|
end
|
|
149
193
|
|
|
150
|
-
def to_s
|
|
151
|
-
message
|
|
152
|
-
end
|
|
153
|
-
|
|
154
194
|
def filter_backtrace(backtrace)
|
|
155
195
|
return backtrace if ENV["RUBY_REACTOR_DEBUG"] == "true"
|
|
156
196
|
return backtrace if backtrace.nil? || backtrace.empty?
|
|
@@ -177,6 +217,26 @@ module RubyReactor
|
|
|
177
217
|
def error_message
|
|
178
218
|
@error.respond_to?(:message) ? @error.message : @error.to_s
|
|
179
219
|
end
|
|
220
|
+
|
|
221
|
+
def extract_attributes_from_hash(error_hash)
|
|
222
|
+
# Ensure indifferent access
|
|
223
|
+
err = ->(k) { error_hash[k.to_s] || error_hash[k.to_sym] }
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
error: err[:message] || err[:error] || error_hash,
|
|
227
|
+
retryable: err[:retryable],
|
|
228
|
+
step_name: err[:step_name],
|
|
229
|
+
reactor_name: err[:reactor_name],
|
|
230
|
+
inputs: err[:inputs] || {},
|
|
231
|
+
step_arguments: err[:step_arguments] || {},
|
|
232
|
+
backtrace: err[:backtrace],
|
|
233
|
+
exception_class: err[:exception_class],
|
|
234
|
+
file_path: err[:file_path],
|
|
235
|
+
line_number: err[:line_number],
|
|
236
|
+
code_snippet: err[:code_snippet],
|
|
237
|
+
validation_errors: err[:validation_errors]
|
|
238
|
+
}
|
|
239
|
+
end
|
|
180
240
|
end
|
|
181
241
|
|
|
182
242
|
# Async result for background job execution
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_reactor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artur
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-validation
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- documentation/images/payment_workflow.png
|
|
109
109
|
- documentation/interrupts.md
|
|
110
110
|
- documentation/retry_configuration.md
|
|
111
|
+
- documentation/testing.md
|
|
111
112
|
- gui/.gitignore
|
|
112
113
|
- gui/README.md
|
|
113
114
|
- gui/eslint.config.js
|
|
@@ -139,7 +140,6 @@ files:
|
|
|
139
140
|
- gui/vite.config.ts
|
|
140
141
|
- gui/vitest.config.ts
|
|
141
142
|
- lib/ruby_reactor.rb
|
|
142
|
-
- lib/ruby_reactor/async_router.rb
|
|
143
143
|
- lib/ruby_reactor/configuration.rb
|
|
144
144
|
- lib/ruby_reactor/context.rb
|
|
145
145
|
- lib/ruby_reactor/context_serializer.rb
|
|
@@ -181,6 +181,12 @@ files:
|
|
|
181
181
|
- lib/ruby_reactor/registry.rb
|
|
182
182
|
- lib/ruby_reactor/retry_context.rb
|
|
183
183
|
- lib/ruby_reactor/retry_queued_result.rb
|
|
184
|
+
- lib/ruby_reactor/rspec.rb
|
|
185
|
+
- lib/ruby_reactor/rspec/helpers.rb
|
|
186
|
+
- lib/ruby_reactor/rspec/matchers.rb
|
|
187
|
+
- lib/ruby_reactor/rspec/step_executor_patch.rb
|
|
188
|
+
- lib/ruby_reactor/rspec/test_subject.rb
|
|
189
|
+
- lib/ruby_reactor/sidekiq_adapter.rb
|
|
184
190
|
- lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb
|
|
185
191
|
- lib/ruby_reactor/sidekiq_workers/map_element_worker.rb
|
|
186
192
|
- lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb
|