simple_flow 0.1.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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.rubocop.yml +57 -0
  5. data/CHANGELOG.md +4 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE +21 -0
  8. data/README.md +481 -0
  9. data/Rakefile +15 -0
  10. data/benchmarks/parallel_vs_sequential.rb +98 -0
  11. data/benchmarks/pipeline_overhead.rb +130 -0
  12. data/docs/api/middleware.md +468 -0
  13. data/docs/api/parallel-step.md +363 -0
  14. data/docs/api/pipeline.md +382 -0
  15. data/docs/api/result.md +375 -0
  16. data/docs/concurrent/best-practices.md +687 -0
  17. data/docs/concurrent/introduction.md +246 -0
  18. data/docs/concurrent/parallel-steps.md +418 -0
  19. data/docs/concurrent/performance.md +481 -0
  20. data/docs/core-concepts/flow-control.md +452 -0
  21. data/docs/core-concepts/middleware.md +389 -0
  22. data/docs/core-concepts/overview.md +219 -0
  23. data/docs/core-concepts/pipeline.md +315 -0
  24. data/docs/core-concepts/result.md +168 -0
  25. data/docs/core-concepts/steps.md +391 -0
  26. data/docs/development/benchmarking.md +443 -0
  27. data/docs/development/contributing.md +380 -0
  28. data/docs/development/dagwood-concepts.md +435 -0
  29. data/docs/development/testing.md +514 -0
  30. data/docs/getting-started/examples.md +197 -0
  31. data/docs/getting-started/installation.md +62 -0
  32. data/docs/getting-started/quick-start.md +218 -0
  33. data/docs/guides/choosing-concurrency-model.md +441 -0
  34. data/docs/guides/complex-workflows.md +440 -0
  35. data/docs/guides/data-fetching.md +478 -0
  36. data/docs/guides/error-handling.md +635 -0
  37. data/docs/guides/file-processing.md +505 -0
  38. data/docs/guides/validation-patterns.md +496 -0
  39. data/docs/index.md +169 -0
  40. data/examples/.gitignore +3 -0
  41. data/examples/01_basic_pipeline.rb +112 -0
  42. data/examples/02_error_handling.rb +178 -0
  43. data/examples/03_middleware.rb +186 -0
  44. data/examples/04_parallel_automatic.rb +221 -0
  45. data/examples/05_parallel_explicit.rb +279 -0
  46. data/examples/06_real_world_ecommerce.rb +288 -0
  47. data/examples/07_real_world_etl.rb +277 -0
  48. data/examples/08_graph_visualization.rb +246 -0
  49. data/examples/09_pipeline_visualization.rb +266 -0
  50. data/examples/10_concurrency_control.rb +235 -0
  51. data/examples/11_sequential_dependencies.rb +243 -0
  52. data/examples/12_none_constant.rb +161 -0
  53. data/examples/README.md +374 -0
  54. data/examples/regression_test/01_basic_pipeline.txt +38 -0
  55. data/examples/regression_test/02_error_handling.txt +92 -0
  56. data/examples/regression_test/03_middleware.txt +61 -0
  57. data/examples/regression_test/04_parallel_automatic.txt +86 -0
  58. data/examples/regression_test/05_parallel_explicit.txt +80 -0
  59. data/examples/regression_test/06_real_world_ecommerce.txt +53 -0
  60. data/examples/regression_test/07_real_world_etl.txt +58 -0
  61. data/examples/regression_test/08_graph_visualization.txt +429 -0
  62. data/examples/regression_test/09_pipeline_visualization.txt +305 -0
  63. data/examples/regression_test/10_concurrency_control.txt +96 -0
  64. data/examples/regression_test/11_sequential_dependencies.txt +86 -0
  65. data/examples/regression_test/12_none_constant.txt +64 -0
  66. data/examples/regression_test.rb +105 -0
  67. data/lib/simple_flow/dependency_graph.rb +120 -0
  68. data/lib/simple_flow/dependency_graph_visualizer.rb +326 -0
  69. data/lib/simple_flow/middleware.rb +36 -0
  70. data/lib/simple_flow/parallel_executor.rb +80 -0
  71. data/lib/simple_flow/pipeline.rb +405 -0
  72. data/lib/simple_flow/result.rb +88 -0
  73. data/lib/simple_flow/step_tracker.rb +58 -0
  74. data/lib/simple_flow/version.rb +5 -0
  75. data/lib/simple_flow.rb +41 -0
  76. data/mkdocs.yml +146 -0
  77. data/pipeline_graph.dot +51 -0
  78. data/pipeline_graph.html +60 -0
  79. data/pipeline_graph.mmd +19 -0
  80. metadata +127 -0
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/simple_flow'
5
+ require 'timecop'
6
+ Timecop.travel(Time.local(2001, 9, 11, 7, 0, 0))
7
+
8
+ # Explicit parallel blocks
9
+ #
10
+ # NOTE: You can control which concurrency model is used with the concurrency parameter:
11
+ # pipeline = SimpleFlow::Pipeline.new(concurrency: :threads) do ... end # Force threads
12
+ # pipeline = SimpleFlow::Pipeline.new(concurrency: :async) do ... end # Force async
13
+ # pipeline = SimpleFlow::Pipeline.new(concurrency: :auto) do ... end # Auto-detect (default)
14
+ #
15
+ # See examples/10_concurrency_control.rb for detailed examples
16
+
17
+ puts "=" * 60
18
+ puts "Explicit Parallel Blocks"
19
+ puts "=" * 60
20
+ puts
21
+
22
+ # Check if async is available
23
+ if SimpleFlow::Pipeline.new.async_available?
24
+ puts "✓ Async gem is available - will use fiber-based concurrency"
25
+ else
26
+ puts "⚠ Async gem not available - will use thread-based parallelism"
27
+ end
28
+ puts
29
+
30
+ # Example 1: Basic parallel block
31
+ puts "Example 1: Basic Parallel Block"
32
+ puts "-" * 60
33
+ puts
34
+
35
+ pipeline = SimpleFlow::Pipeline.new do
36
+ step ->(result) {
37
+ puts " [Sequential] Pre-processing input..."
38
+ result.continue(result.value)
39
+ }
40
+
41
+ parallel do
42
+ step ->(result) {
43
+ puts " [Parallel A] Fetching from API..."
44
+ sleep 0.1
45
+ result.with_context(:api_data, { status: "ok" }).continue(result.value)
46
+ }
47
+
48
+ step ->(result) {
49
+ puts " [Parallel B] Fetching from cache..."
50
+ sleep 0.1
51
+ result.with_context(:cache_data, { cached: true }).continue(result.value)
52
+ }
53
+
54
+ step ->(result) {
55
+ puts " [Parallel C] Fetching from database..."
56
+ sleep 0.1
57
+ result.with_context(:db_data, { records: 10 }).continue(result.value)
58
+ }
59
+ end
60
+
61
+ step ->(result) {
62
+ puts " [Sequential] Post-processing results..."
63
+ merged = {
64
+ api: result.context[:api_data],
65
+ cache: result.context[:cache_data],
66
+ db: result.context[:db_data]
67
+ }
68
+ result.continue(merged)
69
+ }
70
+ end
71
+
72
+ start_time = Time.now
73
+ result = pipeline.call(SimpleFlow::Result.new("data"))
74
+ elapsed = Time.now - start_time
75
+
76
+ puts "\nResult: #{result.value}"
77
+ puts "Execution time: #{(elapsed * 1000).round(2)}ms"
78
+ puts "(Should be ~100ms with parallel, ~300ms sequential)"
79
+ puts
80
+
81
+ # Example 2: Multiple parallel blocks
82
+ puts "\nExample 2: Multiple Parallel Blocks"
83
+ puts "-" * 60
84
+ puts
85
+
86
+ multi_parallel_pipeline = SimpleFlow::Pipeline.new do
87
+ step ->(result) {
88
+ puts " [Step 1] Initialize"
89
+ result.continue(result.value)
90
+ }
91
+
92
+ parallel do
93
+ step ->(result) {
94
+ puts " [Block 1.A] Validate email"
95
+ sleep 0.05
96
+ result.with_context(:email_valid, true).continue(result.value)
97
+ }
98
+
99
+ step ->(result) {
100
+ puts " [Block 1.B] Validate phone"
101
+ sleep 0.05
102
+ result.with_context(:phone_valid, true).continue(result.value)
103
+ }
104
+ end
105
+
106
+ step ->(result) {
107
+ puts " [Step 2] Process validations"
108
+ result.continue(result.value)
109
+ }
110
+
111
+ parallel do
112
+ step ->(result) {
113
+ puts " [Block 2.A] Send email notification"
114
+ sleep 0.05
115
+ result.with_context(:email_sent, true).continue(result.value)
116
+ }
117
+
118
+ step ->(result) {
119
+ puts " [Block 2.B] Send SMS notification"
120
+ sleep 0.05
121
+ result.with_context(:sms_sent, true).continue(result.value)
122
+ }
123
+
124
+ step ->(result) {
125
+ puts " [Block 2.C] Log to database"
126
+ sleep 0.05
127
+ result.with_context(:logged, true).continue(result.value)
128
+ }
129
+ end
130
+
131
+ step ->(result) {
132
+ puts " [Step 3] Finalize"
133
+ result.continue("All notifications sent")
134
+ }
135
+ end
136
+
137
+ start_time = Time.now
138
+ result2 = multi_parallel_pipeline.call(SimpleFlow::Result.new(nil))
139
+ elapsed2 = Time.now - start_time
140
+
141
+ puts "\nResult: #{result2.value}"
142
+ puts "Context: #{result2.context}"
143
+ puts "Execution time: #{(elapsed2 * 1000).round(2)}ms"
144
+ puts
145
+
146
+ # Example 3: Mixing named and parallel blocks
147
+ puts "\nExample 3: Mixed Execution Styles"
148
+ puts "-" * 60
149
+ puts
150
+
151
+ mixed_pipeline = SimpleFlow::Pipeline.new do
152
+ # Traditional unnamed step
153
+ step ->(result) {
154
+ puts " [Unnamed] Starting workflow..."
155
+ result.continue(result.value)
156
+ }
157
+
158
+ # Named steps with dependencies
159
+ step :fetch_config, ->(result) {
160
+ puts " [Named] Fetching configuration..."
161
+ sleep 0.05
162
+ result.with_context(:config, { timeout: 30 }).continue(result.value)
163
+ }, depends_on: :none
164
+
165
+ # Explicit parallel block
166
+ parallel do
167
+ step ->(result) {
168
+ puts " [Parallel] Processing batch 1..."
169
+ sleep 0.05
170
+ result.with_context(:batch1, :done).continue(result.value)
171
+ }
172
+
173
+ step ->(result) {
174
+ puts " [Parallel] Processing batch 2..."
175
+ sleep 0.05
176
+ result.with_context(:batch2, :done).continue(result.value)
177
+ }
178
+ end
179
+
180
+ # Another unnamed step
181
+ step ->(result) {
182
+ puts " [Unnamed] Finalizing..."
183
+ result.continue("Workflow complete")
184
+ }
185
+ end
186
+
187
+ result3 = mixed_pipeline.call(SimpleFlow::Result.new(nil))
188
+ puts "\nResult: #{result3.value}"
189
+ puts "Context: #{result3.context}"
190
+ puts
191
+
192
+ # Example 4: Error handling in parallel blocks
193
+ puts "\nExample 4: Error Handling in Parallel Blocks"
194
+ puts "-" * 60
195
+ puts
196
+
197
+ error_handling_pipeline = SimpleFlow::Pipeline.new do
198
+ step ->(result) {
199
+ puts " [Pre] Starting operations..."
200
+ result.continue(result.value)
201
+ }
202
+
203
+ parallel do
204
+ step ->(result) {
205
+ puts " [Parallel A] Running successfully..."
206
+ sleep 0.05
207
+ result.with_context(:task_a, :success).continue(result.value)
208
+ }
209
+
210
+ step ->(result) {
211
+ puts " [Parallel B] Encountering error..."
212
+ sleep 0.05
213
+ result.halt.with_error(:task_b, "Failed to process batch")
214
+ }
215
+
216
+ step ->(result) {
217
+ puts " [Parallel C] Running successfully..."
218
+ sleep 0.05
219
+ result.with_context(:task_c, :success).continue(result.value)
220
+ }
221
+ end
222
+
223
+ step ->(result) {
224
+ puts " [Post] This should not execute when parallel block halts"
225
+ result.continue("Completed")
226
+ }
227
+ end
228
+
229
+ result4 = error_handling_pipeline.call(SimpleFlow::Result.new(nil))
230
+
231
+ puts "\nResult:"
232
+ puts " Continue? #{result4.continue?}"
233
+ puts " Value: #{result4.value}"
234
+ puts " Errors: #{result4.errors}"
235
+ puts " Context: #{result4.context}"
236
+ puts " Note: Pipeline halted due to error in parallel block"
237
+ puts
238
+
239
+ # Example 5: Performance comparison
240
+ puts "\nExample 5: Performance Comparison"
241
+ puts "-" * 60
242
+ puts
243
+
244
+ # Sequential version
245
+ sequential_pipeline = SimpleFlow::Pipeline.new do
246
+ step ->(result) { sleep 0.1; result.continue(result.value + 1) }
247
+ step ->(result) { sleep 0.1; result.continue(result.value + 1) }
248
+ step ->(result) { sleep 0.1; result.continue(result.value + 1) }
249
+ step ->(result) { sleep 0.1; result.continue(result.value + 1) }
250
+ end
251
+
252
+ # Parallel version
253
+ parallel_pipeline = SimpleFlow::Pipeline.new do
254
+ parallel do
255
+ step ->(result) { sleep 0.1; result.continue(result.value + 1) }
256
+ step ->(result) { sleep 0.1; result.continue(result.value + 1) }
257
+ step ->(result) { sleep 0.1; result.continue(result.value + 1) }
258
+ step ->(result) { sleep 0.1; result.continue(result.value + 1) }
259
+ end
260
+ end
261
+
262
+ puts "Running sequential pipeline (4 steps @ 100ms each)..."
263
+ sequential_start = Time.now
264
+ sequential_result = sequential_pipeline.call(SimpleFlow::Result.new(0))
265
+ sequential_time = Time.now - sequential_start
266
+
267
+ puts "Running parallel pipeline (4 steps @ 100ms each in parallel)..."
268
+ parallel_start = Time.now
269
+ parallel_result = parallel_pipeline.call(SimpleFlow::Result.new(0))
270
+ parallel_time = Time.now - parallel_start
271
+
272
+ puts "\nResults:"
273
+ puts " Sequential: #{(sequential_time * 1000).round(2)}ms (expected ~400ms)"
274
+ puts " Parallel: #{(parallel_time * 1000).round(2)}ms (expected ~100ms)"
275
+ puts " Speedup: #{(sequential_time / parallel_time).round(2)}x"
276
+
277
+ puts "\n" + "=" * 60
278
+ puts "Explicit parallel blocks examples completed!"
279
+ puts "=" * 60
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/simple_flow'
5
+ require 'json'
6
+ require 'timecop'
7
+ Timecop.travel(Time.local(2001, 9, 11, 7, 0, 0))
8
+
9
+ # Real-world example: E-commerce order processing pipeline
10
+
11
+ puts "=" * 60
12
+ puts "Real-World Example: E-commerce Order Processing"
13
+ puts "=" * 60
14
+ puts
15
+
16
+ # Simulate external services
17
+ class InventoryService
18
+ def self.check_availability(product_id)
19
+ sleep 0.05 # Simulate API call
20
+ { product_id: product_id, available: true, quantity: 100 }
21
+ end
22
+
23
+ def self.reserve_items(items)
24
+ sleep 0.05
25
+ { reservation_id: "RES-#{rand(10000)}", items: items }
26
+ end
27
+ end
28
+
29
+ class PaymentService
30
+ def self.process_payment(amount, card_token)
31
+ sleep 0.1 # Simulate payment processing
32
+ if card_token.start_with?("tok_")
33
+ { transaction_id: "TXN-#{rand(10000)}", status: :success, amount: amount }
34
+ else
35
+ { status: :failed, reason: "Invalid card token" }
36
+ end
37
+ end
38
+ end
39
+
40
+ class ShippingService
41
+ def self.calculate_shipping(address, items)
42
+ sleep 0.05
43
+ { cost: 10.00, estimated_days: 3 }
44
+ end
45
+
46
+ def self.create_shipment(order_id, address)
47
+ sleep 0.05
48
+ { tracking_number: "TRACK-#{rand(10000)}", carrier: "FastShip" }
49
+ end
50
+ end
51
+
52
+ class NotificationService
53
+ def self.send_email(to, subject, body)
54
+ sleep 0.02
55
+ puts " 📧 Email sent to #{to}: #{subject}"
56
+ { sent: true, message_id: "MSG-#{rand(10000)}" }
57
+ end
58
+
59
+ def self.send_sms(phone, message)
60
+ sleep 0.02
61
+ puts " 📱 SMS sent to #{phone}: #{message}"
62
+ { sent: true }
63
+ end
64
+ end
65
+
66
+ # Build the order processing pipeline
67
+ order_pipeline = SimpleFlow::Pipeline.new do
68
+ # Step 1: Validate order
69
+ step :validate_order, ->(result) {
70
+ puts " ✓ Validating order..."
71
+ order = result.value
72
+
73
+ # Check required fields
74
+ errors = []
75
+ errors << "Missing customer email" unless order[:customer][:email]
76
+ errors << "No items in order" if order[:items].empty?
77
+ errors << "Missing payment method" unless order[:payment][:card_token]
78
+
79
+ if errors.any?
80
+ return result.halt.with_error(:validation, errors.join(", "))
81
+ end
82
+
83
+ result.with_context(:validated_at, Time.now).continue(order)
84
+ }, depends_on: :none
85
+
86
+ # Step 2 & 3: Run in parallel - check inventory and calculate shipping
87
+ step :check_inventory, ->(result) {
88
+ puts " ✓ Checking inventory..."
89
+ order = result.value
90
+ inventory_results = order[:items].map do |item|
91
+ InventoryService.check_availability(item[:product_id])
92
+ end
93
+
94
+ if inventory_results.all? { |r| r[:available] }
95
+ result.with_context(:inventory_check, inventory_results).continue(order)
96
+ else
97
+ result.halt.with_error(:inventory, "Some items are out of stock")
98
+ end
99
+ }, depends_on: [:validate_order]
100
+
101
+ step :calculate_shipping, ->(result) {
102
+ puts " ✓ Calculating shipping..."
103
+ order = result.value
104
+ shipping = ShippingService.calculate_shipping(
105
+ order[:shipping_address],
106
+ order[:items]
107
+ )
108
+ result.with_context(:shipping, shipping).continue(order)
109
+ }, depends_on: [:validate_order]
110
+
111
+ # Step 4: Calculate totals (waits for inventory and shipping)
112
+ step :calculate_totals, ->(result) {
113
+ puts " ✓ Calculating totals..."
114
+ order = result.value
115
+ shipping = result.context[:shipping]
116
+
117
+ subtotal = order[:items].sum { |item| item[:price] * item[:quantity] }
118
+ tax = subtotal * 0.08
119
+ total = subtotal + tax + shipping[:cost]
120
+
121
+ result
122
+ .with_context(:subtotal, subtotal)
123
+ .with_context(:tax, tax)
124
+ .with_context(:total, total)
125
+ .continue(order)
126
+ }, depends_on: [:check_inventory, :calculate_shipping]
127
+
128
+ # Step 5: Process payment
129
+ step :process_payment, ->(result) {
130
+ puts " ✓ Processing payment..."
131
+ order = result.value
132
+ total = result.context[:total]
133
+
134
+ payment_result = PaymentService.process_payment(
135
+ total,
136
+ order[:payment][:card_token]
137
+ )
138
+
139
+ if payment_result[:status] == :success
140
+ result.with_context(:payment, payment_result).continue(order)
141
+ else
142
+ result.halt.with_error(:payment, payment_result[:reason])
143
+ end
144
+ }, depends_on: [:calculate_totals]
145
+
146
+ # Step 6: Reserve inventory
147
+ step :reserve_inventory, ->(result) {
148
+ puts " ✓ Reserving inventory..."
149
+ order = result.value
150
+ reservation = InventoryService.reserve_items(order[:items])
151
+ result.with_context(:reservation, reservation).continue(order)
152
+ }, depends_on: [:process_payment]
153
+
154
+ # Step 7: Create shipment
155
+ step :create_shipment, ->(result) {
156
+ puts " ✓ Creating shipment..."
157
+ order = result.value
158
+ shipment = ShippingService.create_shipment(
159
+ order[:order_id],
160
+ order[:shipping_address]
161
+ )
162
+ result.with_context(:shipment, shipment).continue(order)
163
+ }, depends_on: [:reserve_inventory]
164
+
165
+ # Step 8 & 9: Send notifications in parallel
166
+ step :send_email_confirmation, ->(result) {
167
+ puts " ✓ Sending email confirmation..."
168
+ order = result.value
169
+ total = result.context[:total]
170
+ tracking = result.context[:shipment][:tracking_number]
171
+
172
+ NotificationService.send_email(
173
+ order[:customer][:email],
174
+ "Order Confirmed",
175
+ "Your order ##{order[:order_id]} for $#{total.round(2)} has been confirmed. Tracking: #{tracking}"
176
+ )
177
+
178
+ result.continue(order)
179
+ }, depends_on: [:create_shipment]
180
+
181
+ step :send_sms_confirmation, ->(result) {
182
+ puts " ✓ Sending SMS confirmation..."
183
+ order = result.value
184
+ tracking = result.context[:shipment][:tracking_number]
185
+
186
+ if order[:customer][:phone]
187
+ NotificationService.send_sms(
188
+ order[:customer][:phone],
189
+ "Order confirmed! Tracking: #{tracking}"
190
+ )
191
+ end
192
+
193
+ result.continue(order)
194
+ }, depends_on: [:create_shipment]
195
+
196
+ # Step 10: Finalize order
197
+ step :finalize_order, ->(result) {
198
+ puts " ✓ Finalizing order..."
199
+ order = result.value
200
+
201
+ final_order = {
202
+ order_id: order[:order_id],
203
+ status: :confirmed,
204
+ total: result.context[:total],
205
+ payment_transaction: result.context[:payment][:transaction_id],
206
+ tracking_number: result.context[:shipment][:tracking_number],
207
+ estimated_delivery_days: result.context[:shipping][:estimated_days]
208
+ }
209
+
210
+ result.continue(final_order)
211
+ }, depends_on: [:send_email_confirmation, :send_sms_confirmation]
212
+ end
213
+
214
+ # Example order data
215
+ order = {
216
+ order_id: "ORD-#{rand(10000)}",
217
+ customer: {
218
+ email: "customer@example.com",
219
+ phone: "+1-555-0123"
220
+ },
221
+ items: [
222
+ { product_id: 101, name: "Widget", price: 29.99, quantity: 2 },
223
+ { product_id: 102, name: "Gadget", price: 49.99, quantity: 1 }
224
+ ],
225
+ shipping_address: {
226
+ street: "123 Main St",
227
+ city: "Springfield",
228
+ state: "IL",
229
+ zip: "62701"
230
+ },
231
+ payment: {
232
+ card_token: "tok_valid_card_123"
233
+ }
234
+ }
235
+
236
+ puts "\nProcessing order: #{order[:order_id]}"
237
+ puts "Items: #{order[:items].size} items totaling $#{order[:items].sum { |i| i[:price] * i[:quantity] }}"
238
+ puts
239
+
240
+ start_time = Time.now
241
+ result = order_pipeline.call_parallel(SimpleFlow::Result.new(order))
242
+ elapsed = Time.now - start_time
243
+
244
+ puts "\n" + "=" * 60
245
+ if result.continue?
246
+ puts "✅ Order processed successfully!"
247
+ puts "=" * 60
248
+ puts "\nOrder Details:"
249
+ puts " Order ID: #{result.value[:order_id]}"
250
+ puts " Status: #{result.value[:status]}"
251
+ puts " Total: $#{result.value[:total].round(2)}"
252
+ puts " Transaction: #{result.value[:payment_transaction]}"
253
+ puts " Tracking: #{result.value[:tracking_number]}"
254
+ puts " Estimated Delivery: #{result.value[:estimated_delivery_days]} days"
255
+ puts "\nProcessing time: #{(elapsed * 1000).round(2)}ms"
256
+ else
257
+ puts "❌ Order processing failed"
258
+ puts "=" * 60
259
+ puts "\nErrors:"
260
+ result.errors.each do |category, messages|
261
+ puts " #{category}: #{messages.join(', ')}"
262
+ end
263
+ end
264
+
265
+ # Test with invalid order
266
+ puts "\n\n" + "=" * 60
267
+ puts "Testing with invalid order (missing payment)..."
268
+ puts "=" * 60
269
+ puts
270
+
271
+ invalid_order = order.dup
272
+ invalid_order[:payment][:card_token] = "invalid_token"
273
+
274
+ result2 = order_pipeline.call_parallel(SimpleFlow::Result.new(invalid_order))
275
+
276
+ if result2.continue?
277
+ puts "✅ Order processed"
278
+ else
279
+ puts "❌ Order failed (as expected)"
280
+ puts "\nErrors:"
281
+ result2.errors.each do |category, messages|
282
+ puts " #{category}: #{messages.join(', ')}"
283
+ end
284
+ end
285
+
286
+ puts "\n" + "=" * 60
287
+ puts "E-commerce example completed!"
288
+ puts "=" * 60