ruby_reactor 0.3.1 → 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.
@@ -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
@@ -46,15 +46,15 @@ class PaymentProcessingReactor < RubyReactor::Reactor
46
46
  # Payment processing needs careful retry configuration
47
47
  retry_defaults max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
48
48
 
49
- input :amount, validate: -> do
49
+ input :amount do
50
50
  required(:amount).filled(:decimal, gt?: 0)
51
51
  end
52
52
 
53
- input :currency, validate: -> do
53
+ input :currency do
54
54
  required(:currency).filled(:string, included_in?: ['USD', 'EUR', 'GBP'])
55
55
  end
56
56
 
57
- input :card_token, validate: -> do
57
+ input :card_token do
58
58
  required(:card_token).filled(:string, format?: /^tok_/)
59
59
  end
60
60
 
@@ -119,10 +119,10 @@ class PaymentProcessingReactor < RubyReactor::Reactor
119
119
  Success({ auth_id: auth_result.id, auth_amount: amount })
120
120
  end
121
121
 
122
- undo do |args, _context|
123
- auth_id = args[:fraud_data][:auth_id] || args[:auth_id]
124
- # Void the pre-authorization
125
- PaymentGateway.void_authorization(auth_id) if auth_id
122
+ undo do |result, _args, _ctx|
123
+ # Void the pre-authorization if a later step fails
124
+ PaymentGateway.void_authorization(result[:auth_id]) if result[:auth_id]
125
+ Success()
126
126
  end
127
127
  end
128
128
 
@@ -148,10 +148,9 @@ class PaymentProcessingReactor < RubyReactor::Reactor
148
148
  Success({ charge_id: charge_result.id, charged_amount: amount })
149
149
  end
150
150
 
151
- undo do |args, _context|
152
- charge_id = args[:auth_data][:charge_id] || args[:charge_id]
153
- # Refund the charge
154
- PaymentGateway.refund(charge_id) if charge_id
151
+ undo do |result, _args, _ctx|
152
+ PaymentGateway.refund(result[:charge_id]) if result[:charge_id]
153
+ Success()
155
154
  end
156
155
  end
157
156
 
@@ -182,10 +181,9 @@ class PaymentProcessingReactor < RubyReactor::Reactor
182
181
  { transaction_id: transaction.id }
183
182
  end
184
183
 
185
- compensate do |args, _context|
186
- transaction_id = args[:charge_data][:transaction_id] || args[:transaction_id]
187
- # Mark transaction as failed/refunded
188
- PaymentTransaction.find_by(id: transaction_id)&.update!(status: :refunded)
184
+ undo do |result, _args, _ctx|
185
+ PaymentTransaction.find_by(id: result[:transaction_id])&.update!(status: :refunded)
186
+ Success()
189
187
  end
190
188
  end
191
189
 
@@ -301,7 +299,7 @@ class SubscriptionPaymentReactor < RubyReactor::Reactor
301
299
 
302
300
  retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 1.hour
303
301
 
304
- input :subscription_id, validate: -> do
302
+ input :subscription_id do
305
303
  required(:subscription_id).filled(:string)
306
304
  end
307
305
 
@@ -351,97 +349,9 @@ class SubscriptionPaymentReactor < RubyReactor::Reactor
351
349
  { charge_id: charge_result.id }
352
350
  end
353
351
 
354
- compensate do |args, _context|
355
- charge_id = args[:validation_data][:charge_id] || args[:charge_id]
356
- # Refund the subscription charge
357
- PaymentGateway.refund_subscription_charge(charge_id) if charge_id
358
- end
359
- end
360
-
361
- step :update_billing_record do
362
- argument :validation_data, result(:validate_subscription)
363
- argument :proration_data, result(:calculate_proration)
364
- argument :charge_data, result(:charge_subscription)
365
-
366
- run do |args, _context|
367
- subscription = args[:validation_data][:subscription]
368
- charge_id = args[:charge_data][:charge_id]
369
- billing_period = args[:proration_data][:billing_period]
370
-
371
- BillingRecord.create!(
372
- subscription: subscription,
373
- charge_id: charge_id,
374
- billing_period: billing_period,
375
- status: :paid
376
- )
377
-
378
- { billing_recorded: true }
379
- end
380
- end
381
- ```
382
-
383
- ### Subscription Payment Processing
384
-
385
- ```ruby
386
- class SubscriptionPaymentReactor < RubyReactor::Reactor
387
- async true
388
-
389
- retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 1.hour
390
-
391
- input :subscription_id, validate: -> do
392
- required(:subscription_id).filled(:string)
393
- end
394
-
395
- step :validate_subscription do
396
- validate_args do
397
- required(:subscription_id).filled(:string)
398
- end
399
-
400
- run do |subscription_id:|
401
- subscription = Subscription.find(subscription_id)
402
- raise "Subscription not found" unless subscription
403
- raise "Subscription inactive" unless subscription.active?
404
-
405
- { subscription: subscription }
406
- end
407
- end
408
-
409
- step :calculate_proration do
410
- argument :validation_data, result(:validate_subscription)
411
-
412
- run do |args, _context|
413
- subscription = args[:validation_data][:subscription]
414
-
415
- # Calculate prorated amount for billing period
416
- proration = BillingService.calculate_proration(subscription)
417
- { proration_amount: proration.amount, billing_period: proration.period }
418
- end
419
- end
420
-
421
- step :charge_subscription do
422
- argument :validation_data, result(:validate_subscription)
423
- argument :proration_data, result(:calculate_proration)
424
-
425
- run do |args, _context|
426
- subscription = args[:validation_data][:subscription]
427
- proration_amount = args[:proration_data][:proration_amount]
428
- billing_period = args[:proration_data][:billing_period]
429
-
430
- charge_result = PaymentGateway.charge_subscription(
431
- customer_id: subscription.customer.stripe_id,
432
- amount: proration_amount,
433
- description: "Subscription #{subscription.id} - #{billing_period}"
434
- )
435
-
436
- raise "Subscription charge failed" unless charge_result.success?
437
-
438
- { charge_id: charge_result.id }
439
- end
440
-
441
- compensate do |args, _context|
442
- charge_id = args[:validation_data][:charge_id] || args[:charge_id]
443
- # Refund the subscription charge
444
- PaymentGateway.refund_subscription_charge(charge_id) if charge_id
352
+ undo do |result, _args, _ctx|
353
+ PaymentGateway.refund_subscription_charge(result[:charge_id]) if result[:charge_id]
354
+ Success()
445
355
  end
446
356
  end
447
357
 
@@ -541,10 +451,10 @@ RSpec.describe PaymentProcessingReactor do
541
451
  allow(PaymentGateway).to receive(:charge).and_return(successful_charge)
542
452
  allow(EmailService).to receive(:send_payment_receipt).and_return(successful_email)
543
453
 
544
- result = PaymentProcessingReactor.run(valid_payment_params)
454
+ subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
545
455
 
546
- expect(result).to be_success
547
- expect(result.step_results[:charge_payment][:charge_id]).to be_present
456
+ expect(subject).to be_success
457
+ expect(subject.step_result(:charge_payment)[:charge_id]).to be_present
548
458
  end
549
459
  end
550
460
 
@@ -552,10 +462,10 @@ RSpec.describe PaymentProcessingReactor do
552
462
  it "blocks high-risk payments" do
553
463
  allow(FraudDetectionService).to receive(:analyze).and_return(0.9)
554
464
 
555
- result = PaymentProcessingReactor.run(valid_payment_params)
465
+ subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
556
466
 
557
- expect(result).to be_failure
558
- expect(result.error.message).to include("High fraud risk")
467
+ expect(subject).to be_failure
468
+ expect(subject.error).to include("High fraud risk")
559
469
  end
560
470
  end
561
471
 
@@ -569,9 +479,10 @@ RSpec.describe PaymentProcessingReactor do
569
479
 
570
480
  expect(PaymentGateway).to receive(:pre_authorize).exactly(3).times
571
481
 
572
- result = PaymentProcessingReactor.run(valid_payment_params)
482
+ subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
573
483
 
574
- expect(result).to be_success
484
+ expect(subject).to be_success
485
+ expect(subject).to have_retried_step(:pre_authorize).times(2)
575
486
  end
576
487
  end
577
488
 
@@ -583,9 +494,9 @@ RSpec.describe PaymentProcessingReactor do
583
494
 
584
495
  expect(PaymentGateway).to receive(:void_authorization)
585
496
 
586
- result = PaymentProcessingReactor.run(valid_payment_params)
497
+ subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
587
498
 
588
- expect(result).to be_failure
499
+ expect(subject).to be_failure
589
500
  end
590
501
  end
591
502
  end