ruby_reactor 0.3.0 → 0.3.2

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -9
  3. data/documentation/README.md +20 -8
  4. data/documentation/async_reactors.md +46 -34
  5. data/documentation/core_concepts.md +75 -61
  6. data/documentation/examples/inventory_management.md +2 -3
  7. data/documentation/examples/order_processing.md +92 -77
  8. data/documentation/examples/payment_processing.md +28 -117
  9. data/documentation/getting_started.md +112 -94
  10. data/documentation/interrupts.md +9 -7
  11. data/documentation/locks_and_semaphores.md +459 -0
  12. data/documentation/retry_configuration.md +19 -14
  13. data/documentation/testing.md +994 -0
  14. data/lib/ruby_reactor/configuration.rb +19 -2
  15. data/lib/ruby_reactor/context.rb +13 -5
  16. data/lib/ruby_reactor/context_serializer.rb +55 -4
  17. data/lib/ruby_reactor/dsl/lockable.rb +130 -0
  18. data/lib/ruby_reactor/dsl/reactor.rb +3 -2
  19. data/lib/ruby_reactor/error/step_failure_error.rb +5 -2
  20. data/lib/ruby_reactor/executor/result_handler.rb +27 -2
  21. data/lib/ruby_reactor/executor/retry_manager.rb +15 -7
  22. data/lib/ruby_reactor/executor/step_executor.rb +29 -99
  23. data/lib/ruby_reactor/executor.rb +148 -15
  24. data/lib/ruby_reactor/lock.rb +92 -0
  25. data/lib/ruby_reactor/map/collector.rb +16 -15
  26. data/lib/ruby_reactor/map/element_executor.rb +90 -104
  27. data/lib/ruby_reactor/map/execution.rb +2 -1
  28. data/lib/ruby_reactor/map/helpers.rb +2 -1
  29. data/lib/ruby_reactor/map/result_enumerator.rb +1 -1
  30. data/lib/ruby_reactor/period.rb +67 -0
  31. data/lib/ruby_reactor/rate_limit.rb +74 -0
  32. data/lib/ruby_reactor/reactor.rb +175 -16
  33. data/lib/ruby_reactor/rspec/helpers.rb +17 -0
  34. data/lib/ruby_reactor/rspec/matchers.rb +423 -0
  35. data/lib/ruby_reactor/rspec/step_executor_patch.rb +85 -0
  36. data/lib/ruby_reactor/rspec/test_subject.rb +625 -0
  37. data/lib/ruby_reactor/rspec.rb +18 -0
  38. data/lib/ruby_reactor/semaphore.rb +58 -0
  39. data/lib/ruby_reactor/{async_router.rb → sidekiq_adapter.rb} +10 -5
  40. data/lib/ruby_reactor/sidekiq_workers/worker.rb +69 -9
  41. data/lib/ruby_reactor/step/compose_step.rb +0 -1
  42. data/lib/ruby_reactor/step/map_step.rb +11 -18
  43. data/lib/ruby_reactor/storage/redis_adapter.rb +2 -0
  44. data/lib/ruby_reactor/storage/redis_locking.rb +251 -0
  45. data/lib/ruby_reactor/version.rb +1 -1
  46. data/lib/ruby_reactor/web/api.rb +32 -24
  47. data/lib/ruby_reactor.rb +119 -10
  48. metadata +16 -3
@@ -24,12 +24,15 @@ Steps are the individual units of work within a reactor. Each step has a name an
24
24
 
25
25
  ### Inline Step Definition
26
26
 
27
+ `run` blocks always receive two positional arguments: the resolved arguments hash and the execution context. Declare inputs with `argument :name, source`:
28
+
27
29
  ```ruby
28
30
  step :validate_order do
29
- run do |order_id|
30
- # Step implementation
31
- order = Order.find(order_id)
32
- raise "Order not found" unless order
31
+ argument :order_id, input(:order_id)
32
+
33
+ run do |args, _context|
34
+ order = Order.find(args[:order_id])
35
+ return Failure("Order not found") unless order
33
36
  Success({ order: order })
34
37
  end
35
38
  end
@@ -144,29 +147,22 @@ end
144
147
 
145
148
  ## Results
146
149
 
147
- Every reactor execution returns a comprehensive result object.
148
- <!--
149
- # TODO
150
-
151
- This is not true, update to use instance and what is stored
152
- ```ruby
153
- result = OrderProcessingReactor.run(order_id: 123)
154
-
155
- # Overall status
156
- result.success? # => true/false
157
- result.failure? # => true/false
150
+ `Reactor.run` returns one of four result types:
158
151
 
159
- # Step outputs
160
- result.step_results # => { validate_order: {...}, process_payment: {...} }
161
- result.intermediate_results # => Hash of all step outputs
152
+ - **`RubyReactor::Success`** — `success?` is `true`. `value` holds the output of the step named in `returns`, or the full `intermediate_results` hash if no `returns` is declared.
153
+ - **`RubyReactor::Failure`** `failure?` is `true`. Readers include `error`, `step_name`, `reactor_name`, `step_arguments`, `inputs`, `exception_class`, `file_path`, `line_number`, `backtrace`, `validation_errors`, and `retryable?`.
154
+ - **`RubyReactor::AsyncResult`** returned by an async reactor or when a step hands off to a worker. Readers: `job_id`, `execution_id`, `intermediate_results`.
155
+ - **`RubyReactor::InterruptResult`** — returned when an `interrupt` step pauses execution. Readers: `execution_id`, `correlation_id`, `status` (`:paused`), `timeout_at`, `intermediate_results`.
162
156
 
163
- # Execution tracking
164
- result.completed_steps # => #<Set: {:validate_order, :process_payment}>
165
- result.inputs # => { order_id: 123 }
157
+ Step-by-step state lives on the context, not the result object. Reload via `Reactor.find(execution_id)` to inspect:
166
158
 
167
- # Error information
168
- result.error # => Exception object if failed
169
- ``` -->
159
+ ```ruby
160
+ reactor = OrderProcessingReactor.find(execution_id)
161
+ reactor.context.intermediate_results # => { validate_order: {...}, ... }
162
+ reactor.context.status # => "completed" | "failed" | "paused" | "running"
163
+ reactor.execution_trace # ordered list of run/undo/compensate entries
164
+ reactor.result # reconstructed Success/Failure/InterruptResult
165
+ ```
170
166
 
171
167
  ## Error Handling
172
168
 
@@ -178,14 +174,18 @@ When a step fails, execution stops and compensation begins:
178
174
 
179
175
  ```ruby
180
176
  step :process_payment do
181
- run do
177
+ argument :amount, input(:amount)
178
+ argument :token, input(:card_token)
179
+
180
+ run do |args, _ctx|
182
181
  # This might fail
183
- PaymentService.charge(amount, token)
182
+ PaymentService.charge(args[:amount], args[:token])
184
183
  end
185
184
 
186
- compensate do |payment_id: nil, **|
187
- # Undo the payment if it was created
188
- PaymentService.refund(payment_id) if payment_id
185
+ compensate do |error, args, _ctx|
186
+ # Compensation receives (error, arguments, context)
187
+ # Best-effort cleanup specific to this step's failure
188
+ AuditService.log_payment_failure(args[:token], error.message)
189
189
  end
190
190
  end
191
191
  ```
@@ -349,22 +349,23 @@ result = Reactor.run(inputs)
349
349
 
350
350
  ### Asynchronous Execution
351
351
 
352
- <!-- TODO: review this part -->
353
-
354
352
  ```ruby
355
353
  async_result = Reactor.run(inputs)
356
354
  # Returns immediately
357
- # Check status later
358
-
359
- case async_result.status
360
- when :success
361
- result = async_result.result
362
- when :failed
363
- error = async_result.error
355
+ async_result.execution_id # UUID to look up state later
356
+
357
+ # Reload to inspect status / final result
358
+ reactor = Reactor.find(async_result.execution_id)
359
+ case reactor.context.status.to_s
360
+ when "completed" then reactor.result.value
361
+ when "failed" then reactor.result.error
362
+ when "paused" then reactor.result.correlation_id
363
+ when "running" then :still_running
364
364
  end
365
365
  ```
366
366
 
367
367
  **Characteristics:**
368
+
368
369
  - Non-blocking execution
369
370
  - Background processing with Sidekiq
370
371
  - Retry capabilities
@@ -372,33 +373,38 @@ end
372
373
 
373
374
  ## Step Arguments
374
375
 
375
- Steps receive arguments through keyword arguments, with automatic dependency injection.
376
+ `run` blocks always receive two positional arguments: the resolved arguments hash and the context. Declare each argument explicitly with `argument :name, source` — there is no implicit keyword injection.
377
+
378
+ Sources you can use:
379
+
380
+ - `input(:name)` — value from the reactor's inputs (the hash passed to `Reactor.run`).
381
+ - `input(:name, :path)` — nested path access into a hash input.
382
+ - `result(:step_name)` — full output of a previous step.
383
+ - `result(:step_name, :path)` — nested path into a previous step's output.
384
+ - `value(literal)` — a constant value.
376
385
 
377
386
  ```ruby
378
387
  step :validate_order do
379
- run do |order_id:, customer_id:|
380
- # Direct access to reactor inputs
381
- order = Order.find_by(id: order_id, customer_id: customer_id)
388
+ argument :order_id, input(:order_id)
389
+ argument :customer_id, input(:customer_id)
390
+
391
+ run do |args, _context|
392
+ order = Order.find_by(id: args[:order_id], customer_id: args[:customer_id])
382
393
  Success({ order: order })
383
394
  end
384
395
  end
385
396
 
386
397
  step :process_payment do
387
- argument :order_data, result(:validate_order)
398
+ argument :order, result(:validate_order, :order)
388
399
 
389
400
  run do |args, _context|
390
- # Access results from previous steps
391
- order = args[:order_data][:order]
392
- payment = PaymentService.charge(order.total, order.card_token)
401
+ payment = PaymentService.charge(args[:order].total, args[:order].card_token)
393
402
  Success({ payment_id: payment.id })
394
403
  end
395
404
  end
396
405
  ```
397
406
 
398
- **Argument Resolution:**
399
- 1. **Step Results**: Outputs from completed dependent steps
400
- 2. **Reactor Inputs**: Original inputs passed to `run()`
401
- 3. **Intermediate Results**: Accumulated outputs from all steps
407
+ If a step declares no `argument`s, the reactor's raw inputs hash is passed as `args`.
402
408
 
403
409
  ## Undo
404
410
 
@@ -415,15 +421,16 @@ Unlike compensation which only runs for the failing step, undo is triggered duri
415
421
 
416
422
  ```ruby
417
423
  step :reserve_inventory do
418
- run do |items:|
419
- reservation_id = InventoryService.reserve(items)
424
+ argument :items, input(:items)
425
+
426
+ run do |args, _ctx|
427
+ reservation_id = InventoryService.reserve(args[:items])
420
428
  Success({ reservation_id: reservation_id })
421
429
  end
422
430
 
423
- undo do |reservation_result, arguments, context|
431
+ undo do |result, arguments, context|
424
432
  # Undo receives the step's result, arguments, and full context
425
- reservation_id = reservation_result[:reservation_id]
426
- InventoryService.release(reservation_id)
433
+ InventoryService.release(result[:reservation_id])
427
434
  Success("Inventory reservation released")
428
435
  end
429
436
  end
@@ -438,9 +445,11 @@ Undo blocks receive three parameters:
438
445
 
439
446
  ```ruby
440
447
  step :complex_operation do
441
- run do |input:|
448
+ argument :input, input(:payload)
449
+
450
+ run do |args, _ctx|
442
451
  # Complex operation that modifies external state
443
- record = create_record(input)
452
+ record = create_record(args[:input])
444
453
  notification = send_notification(record)
445
454
  Success({ record_id: record.id, notification_id: notification.id })
446
455
  end
@@ -477,8 +486,10 @@ Compensation runs immediately when a step fails, before the broader rollback pro
477
486
 
478
487
  ```ruby
479
488
  step :reserve_inventory do
480
- run do |items:|
481
- reservation_id = InventoryService.reserve(items)
489
+ argument :items, input(:items)
490
+
491
+ run do |args, _ctx|
492
+ reservation_id = InventoryService.reserve(args[:items])
482
493
  Success({ reservation_id: reservation_id })
483
494
  end
484
495
 
@@ -501,9 +512,12 @@ Compensation blocks receive three parameters:
501
512
 
502
513
  ```ruby
503
514
  step :process_payment do
504
- run do |order:, payment_method:|
515
+ argument :order, result(:validate_order)
516
+ argument :payment_method, input(:payment_method)
517
+
518
+ run do |args, _ctx|
505
519
  # Payment processing logic that might fail
506
- PaymentService.charge(order.total, payment_method)
520
+ PaymentService.charge(args[:order].total, args[:payment_method])
507
521
  end
508
522
 
509
523
  compensate do |error, arguments, context|
@@ -198,7 +198,6 @@ class InventoryManagementReactor < RubyReactor::Reactor
198
198
  argument :request_data, result(:validate_request)
199
199
  argument :replenishment_data, result(:check_replenishment)
200
200
 
201
- idempotent true
202
201
  retries max_attempts: 3, backoff: :linear, base_delay: 5.seconds
203
202
 
204
203
  run do |args, _context|
@@ -283,7 +282,7 @@ class BulkInventoryReactor < RubyReactor::Reactor
283
282
  if result.success?
284
283
  results << { success: true, operation: op }
285
284
  else
286
- results << { success: false, operation: op, error: result.error.message }
285
+ results << { success: false, operation: op, error: result.error.to_s }
287
286
  end
288
287
  rescue => e
289
288
  results << { success: false, operation: op, error: e.message }
@@ -629,7 +628,7 @@ RSpec.describe InventoryManagementReactor do
629
628
  )
630
629
 
631
630
  expect(result).to be_failure
632
- expect(result.error.message).to include("Insufficient stock")
631
+ expect(result.error).to include("Insufficient stock")
633
632
  end
634
633
  end
635
634
 
@@ -46,28 +46,30 @@ class OrderProcessingReactor < RubyReactor::Reactor
46
46
  # Reactor-level retry defaults
47
47
  retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 2.seconds
48
48
 
49
+ input :order_id do
50
+ required(:order_id).filled(:string)
51
+ end
52
+
49
53
  step :validate_order do
50
- validate_args do
51
- required(:order_id).filled(:string)
52
- end
54
+ argument :order_id, input(:order_id)
53
55
 
54
- run do |order_id:|
55
- order = Order.find_by(id: order_id)
56
- raise "Order not found" unless order
57
- raise "Order already processed" if order.processed?
58
- raise "Order cancelled" if order.cancelled?
56
+ run do |args, _ctx|
57
+ order = Order.find_by(id: args[:order_id])
58
+ return Failure("Order not found") unless order
59
+ return Failure("Order already processed") if order.processed?
60
+ return Failure("Order cancelled") if order.cancelled?
59
61
 
60
62
  Success({ order: order })
61
63
  end
62
64
  end
63
65
 
64
66
  step :check_inventory do
65
- argument :order, result(:validate_order)
67
+ argument :order, result(:validate_order, :order)
66
68
 
67
- run do |order:, **|
69
+ run do |args, _ctx|
68
70
  unavailable_items = []
69
71
 
70
- order.items.each do |item|
72
+ args[:order].items.each do |item|
71
73
  product = Product.find(item.product_id)
72
74
  if product.inventory_count < item.quantity
73
75
  unavailable_items << {
@@ -78,35 +80,37 @@ class OrderProcessingReactor < RubyReactor::Reactor
78
80
  end
79
81
  end
80
82
 
81
- raise "Insufficient inventory: #{unavailable_items}" unless unavailable_items.empty?
83
+ return Failure("Insufficient inventory: #{unavailable_items}") unless unavailable_items.empty?
82
84
 
83
85
  Success({ inventory_checked: true })
84
86
  end
85
87
  end
86
88
 
87
89
  step :reserve_inventory do
88
- argument :order, result(:validate_order)
90
+ argument :order, result(:validate_order, :order)
89
91
 
90
- run do |order:, **|
91
- reservation_id = InventoryService.reserve_items(order.items)
92
- raise "Inventory reservation failed" unless reservation_id
92
+ run do |args, _ctx|
93
+ reservation_id = InventoryService.reserve_items(args[:order].items)
94
+ return Failure("Inventory reservation failed") unless reservation_id
93
95
 
94
96
  Success({ reservation_id: reservation_id })
95
97
  end
96
98
 
97
- undo do |reservation_id:, **|
98
- # Release reservation on failure
99
- InventoryService.release_reservation(reservation_id) if reservation_id
99
+ undo do |result, _args, _ctx|
100
+ # Release reservation when a later step fails
101
+ InventoryService.release_reservation(result[:reservation_id]) if result[:reservation_id]
102
+ Success()
100
103
  end
101
104
  end
102
105
 
103
106
  step :process_payment do
104
- argument :order, result(:validate_order)
107
+ argument :order, result(:validate_order, :order)
105
108
 
106
109
  # Payment processing needs careful retry handling
107
110
  retries max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
108
111
 
109
- run do |order:, **|
112
+ run do |args, _ctx|
113
+ order = args[:order]
110
114
  payment_result = PaymentService.charge(
111
115
  amount: order.total,
112
116
  currency: order.currency,
@@ -114,44 +118,42 @@ class OrderProcessingReactor < RubyReactor::Reactor
114
118
  description: "Order ##{order.id}"
115
119
  )
116
120
 
117
- raise "Payment failed: #{payment_result.error}" unless payment_result.success?
121
+ return Failure("Payment failed: #{payment_result.error}") unless payment_result.success?
118
122
 
119
123
  Success({ payment_id: payment_result.id, payment_amount: order.total })
120
124
  end
121
125
 
122
- undo do |payment_id:, **|
123
- # Refund payment on failure
124
- PaymentService.refund(payment_id) if payment_id
126
+ undo do |result, _args, _ctx|
127
+ PaymentService.refund(result[:payment_id]) if result[:payment_id]
128
+ Success()
125
129
  end
126
130
  end
127
131
 
128
132
  step :update_inventory do
129
- argument :order, result(:validate_order)
130
- argument :reservation_id, result(:reserve_inventory)
133
+ argument :order, result(:validate_order, :order)
134
+ argument :reservation_id, result(:reserve_inventory, :reservation_id)
131
135
 
132
- run do |order:, reservation_id:, **|
133
- # Convert reservation to permanent inventory reduction
134
- success = InventoryService.confirm_reservation(reservation_id)
135
- raise "Inventory update failed" unless success
136
+ run do |args, _ctx|
137
+ success = InventoryService.confirm_reservation(args[:reservation_id])
138
+ return Failure("Inventory update failed") unless success
136
139
 
137
140
  Success({ inventory_updated: true })
138
141
  end
139
142
 
140
- undo do |order:, reservation_id:, **|
141
- # This shouldn't normally happen since payment succeeded
142
- # But if it does, we need to restore inventory
143
- InventoryService.restore_from_reservation(reservation_id) if reservation_id
143
+ undo do |_result, args, _ctx|
144
+ InventoryService.restore_from_reservation(args[:reservation_id]) if args[:reservation_id]
145
+ Success()
144
146
  end
145
147
  end
146
148
 
147
149
  step :update_order_status do
148
- argument :order, result(:validate_order)
149
- argument :payment_id, result(:process_payment)
150
+ argument :order, result(:validate_order, :order)
151
+ argument :payment_id, result(:process_payment, :payment_id)
150
152
 
151
- run do |order:, payment_id:, **|
152
- order.update!(
153
+ run do |args, _ctx|
154
+ args[:order].update!(
153
155
  status: :completed,
154
- payment_id: payment_id,
156
+ payment_id: args[:payment_id],
155
157
  processed_at: Time.current
156
158
  )
157
159
 
@@ -160,23 +162,25 @@ class OrderProcessingReactor < RubyReactor::Reactor
160
162
  end
161
163
 
162
164
  step :send_confirmation do
163
- argument :order, result(:validate_order)
164
- argument :payment_id, result(:process_payment)
165
+ argument :order, result(:validate_order, :order)
166
+ argument :payment_id, result(:process_payment, :payment_id)
165
167
 
166
168
  retries max_attempts: 3, backoff: :linear, base_delay: 10.seconds
167
169
 
168
- run do |order:, payment_id:, **|
170
+ run do |args, _ctx|
169
171
  email_result = EmailService.send_order_confirmation(
170
- to: order.customer.email,
171
- order: order,
172
- payment_id: payment_id
172
+ to: args[:order].customer.email,
173
+ order: args[:order],
174
+ payment_id: args[:payment_id]
173
175
  )
174
176
 
175
- raise "Confirmation email failed" unless email_result.success?
177
+ return Failure("Confirmation email failed") unless email_result.success?
176
178
 
177
179
  Success({ confirmation_sent: true })
178
180
  end
179
181
  end
182
+
183
+ returns :send_confirmation
180
184
  end
181
185
  ```
182
186
 
@@ -187,16 +191,21 @@ end
187
191
  ```ruby
188
192
  # Start order processing asynchronously
189
193
  async_result = OrderProcessingReactor.run(order_id: 12345)
194
+ async_result.execution_id # UUID for state lookup
190
195
 
191
- # Check status later
192
- case async_result.status
193
- when :success
196
+ # Reload state later (e.g. from a polling endpoint)
197
+ reactor = OrderProcessingReactor.find(async_result.execution_id)
198
+ case reactor.context.status.to_s
199
+ when "completed"
194
200
  puts "Order processed successfully!"
195
- result = async_result.result
196
- puts "Payment ID: #{result.step_results[:process_payment][:payment_id]}"
197
- when :failed
198
- puts "Order processing failed: #{async_result.error.message}"
201
+ payment_id = reactor.context.intermediate_results[:process_payment][:payment_id]
202
+ puts "Payment ID: #{payment_id}"
203
+ when "failed"
204
+ failure = reactor.result # RubyReactor::Failure
205
+ puts "Order processing failed at #{failure.step_name}: #{failure.error}"
199
206
  # Could trigger manual review process
207
+ when "running"
208
+ puts "Still processing..."
200
209
  end
201
210
  ```
202
211
 
@@ -204,14 +213,15 @@ end
204
213
 
205
214
  ```ruby
206
215
  # For testing or immediate processing
207
- result = OrderProcessingReactor.run(order_id: 12345)
216
+ reactor = OrderProcessingReactor.new
217
+ result = reactor.run(order_id: 12345)
208
218
 
209
219
  if result.success?
210
220
  puts "Order completed!"
211
- puts "Steps completed: #{result.completed_steps.to_a}"
221
+ puts "Steps completed: #{reactor.context.intermediate_results.keys}"
212
222
  else
213
- puts "Failed at step: #{result.error.step_name}"
214
- puts "Error: #{result.error.message}"
223
+ puts "Failed at step: #{result.step_name}"
224
+ puts "Error: #{result.error}"
215
225
  end
216
226
  ```
217
227
 
@@ -256,10 +266,10 @@ RSpec.describe OrderProcessingReactor do
256
266
  allow(InventoryService).to receive(:confirm_reservation).and_return(true)
257
267
  allow(EmailService).to receive(:send_order_confirmation).and_return(successful_email)
258
268
 
259
- result = OrderProcessingReactor.run(order_id: order.id)
269
+ subject = test_reactor(OrderProcessingReactor, order_id: order.id)
260
270
 
261
- expect(result).to be_success
262
- expect(result.completed_steps).to include(:send_confirmation)
271
+ expect(subject).to be_success
272
+ expect(subject).to have_run_step(:send_confirmation)
263
273
  end
264
274
  end
265
275
 
@@ -271,10 +281,10 @@ RSpec.describe OrderProcessingReactor do
271
281
 
272
282
  expect(InventoryService).to receive(:release_reservation).with("res_123")
273
283
 
274
- result = OrderProcessingReactor.run(order_id: order.id)
284
+ subject = test_reactor(OrderProcessingReactor, order_id: order.id)
275
285
 
276
- expect(result).to be_failure
277
- expect(result.error.message).to include("Payment failed")
286
+ expect(subject).to be_failure
287
+ expect(subject.error).to include("Payment failed")
278
288
  end
279
289
  end
280
290
  end
@@ -315,15 +325,16 @@ email_delivery_failure_rate
315
325
  class PartialOrderProcessingReactor < OrderProcessingReactor
316
326
  # Override to allow partial fulfillment
317
327
  step :check_inventory do
318
- run do |order:, **|
319
- available_items, unavailable_items = partition_available_items(order.items)
328
+ argument :order, result(:validate_order, :order)
329
+
330
+ run do |args, _ctx|
331
+ available_items, unavailable_items = partition_available_items(args[:order].items)
320
332
 
321
333
  if available_items.any? && unavailable_items.any?
322
- # Create partial order for available items
323
- partial_order = create_partial_order(order, available_items)
334
+ partial_order = create_partial_order(args[:order], available_items)
324
335
  Success({ partial_order: partial_order, unavailable_items: unavailable_items })
325
336
  elsif available_items.empty?
326
- raise "No items available"
337
+ Failure("No items available")
327
338
  else
328
339
  Success({ inventory_checked: true })
329
340
  end
@@ -336,23 +347,27 @@ end
336
347
 
337
348
  ```ruby
338
349
  class OrderCancellationReactor < RubyReactor::Reactor
350
+ input :order_id do
351
+ required(:order_id).filled(:string)
352
+ end
353
+
339
354
  step :load_order do
340
- validate_args do
341
- required(:order_id).filled(:string)
342
- end
355
+ argument :order_id, input(:order_id)
343
356
 
344
- run do |order_id:|
345
- order = Order.find_by(id: order_id)
346
- raise "Order not found" unless order
357
+ run do |args, _ctx|
358
+ order = Order.find_by(id: args[:order_id])
359
+ return Failure("Order not found") unless order
347
360
  Success({ order: order })
348
361
  end
349
362
  end
350
363
 
351
364
  step :cancel_order do
352
- run do |order:, **|
365
+ argument :order, result(:load_order, :order)
366
+
367
+ run do |args, _ctx|
368
+ order = args[:order]
353
369
  # Only cancel if not already completed
354
370
  if order.completed?
355
- # Initiate refund and inventory restoration
356
371
  PaymentService.refund(order.payment_id)
357
372
  InventoryService.restore_order_items(order)
358
373
  end