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.
@@ -39,9 +39,7 @@ module RubyReactor
39
39
  executor = Executor.new(context.reactor_class, {}, context)
40
40
  executor.resume_execution
41
41
  executor.save_context
42
-
43
- # Return the executor (which now has the result stored in it)
44
- executor
42
+ executor.result
45
43
  end
46
44
 
47
45
  private
@@ -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
- arguments[:async] && !context.inline_async_execution
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, fail_fast = true)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyReactor
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -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
- data = RubyReactor::Configuration.instance.storage_adapter.find_context_by_id(reactor_id)
28
- return { error: "Reactor not found" } unless data
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
- reactor_class = data["reactor_class"] ? Object.const_get(data["reactor_class"]) : nil
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["context_id"],
37
- class: data["reactor_class"],
38
- status: if %w[failed paused completed running].include?(data["status"])
39
- data["status"]
40
- elsif data["cancelled"]
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["current_step"] ? "running" : "completed")
46
+ (data[:current_step] ? "running" : "completed")
44
47
  end,
45
- current_step: data["current_step"],
46
- retry_count: data["retry_count"] || 0,
47
- undo_stack: data["undo_stack"] || [],
48
- step_attempts: data.dig("retry_context", "step_attempts") || {},
49
- created_at: data["started_at"],
50
- inputs: data["inputs"],
51
- intermediate_results: data["intermediate_results"],
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["execution_trace"] || [],
56
+ steps: data[:execution_trace] || [],
54
57
  composed_contexts: self.class.hydrate_composed_contexts(
55
- data["composed_contexts"] || {},
56
- data["reactor_class"]
58
+ data[:composed_contexts] || {},
59
+ data[:reactor_class]&.to_s
57
60
  ),
58
- error: data["failure_reason"]
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
- if ["map_ref", :map_ref].include?(value["type"])
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["element_reactor_class"] || reactor_class_name
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 = backtrace || (error.respond_to?(:backtrace) ? error.backtrace : caller)
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.0
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-13 00:00:00.000000000 Z
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