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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +4 -0
- data/COMMITS.md +196 -0
- data/LICENSE +21 -0
- data/README.md +481 -0
- data/Rakefile +15 -0
- data/benchmarks/parallel_vs_sequential.rb +98 -0
- data/benchmarks/pipeline_overhead.rb +130 -0
- data/docs/api/middleware.md +468 -0
- data/docs/api/parallel-step.md +363 -0
- data/docs/api/pipeline.md +382 -0
- data/docs/api/result.md +375 -0
- data/docs/concurrent/best-practices.md +687 -0
- data/docs/concurrent/introduction.md +246 -0
- data/docs/concurrent/parallel-steps.md +418 -0
- data/docs/concurrent/performance.md +481 -0
- data/docs/core-concepts/flow-control.md +452 -0
- data/docs/core-concepts/middleware.md +389 -0
- data/docs/core-concepts/overview.md +219 -0
- data/docs/core-concepts/pipeline.md +315 -0
- data/docs/core-concepts/result.md +168 -0
- data/docs/core-concepts/steps.md +391 -0
- data/docs/development/benchmarking.md +443 -0
- data/docs/development/contributing.md +380 -0
- data/docs/development/dagwood-concepts.md +435 -0
- data/docs/development/testing.md +514 -0
- data/docs/getting-started/examples.md +197 -0
- data/docs/getting-started/installation.md +62 -0
- data/docs/getting-started/quick-start.md +218 -0
- data/docs/guides/choosing-concurrency-model.md +441 -0
- data/docs/guides/complex-workflows.md +440 -0
- data/docs/guides/data-fetching.md +478 -0
- data/docs/guides/error-handling.md +635 -0
- data/docs/guides/file-processing.md +505 -0
- data/docs/guides/validation-patterns.md +496 -0
- data/docs/index.md +169 -0
- data/examples/.gitignore +3 -0
- data/examples/01_basic_pipeline.rb +112 -0
- data/examples/02_error_handling.rb +178 -0
- data/examples/03_middleware.rb +186 -0
- data/examples/04_parallel_automatic.rb +221 -0
- data/examples/05_parallel_explicit.rb +279 -0
- data/examples/06_real_world_ecommerce.rb +288 -0
- data/examples/07_real_world_etl.rb +277 -0
- data/examples/08_graph_visualization.rb +246 -0
- data/examples/09_pipeline_visualization.rb +266 -0
- data/examples/10_concurrency_control.rb +235 -0
- data/examples/11_sequential_dependencies.rb +243 -0
- data/examples/12_none_constant.rb +161 -0
- data/examples/README.md +374 -0
- data/examples/regression_test/01_basic_pipeline.txt +38 -0
- data/examples/regression_test/02_error_handling.txt +92 -0
- data/examples/regression_test/03_middleware.txt +61 -0
- data/examples/regression_test/04_parallel_automatic.txt +86 -0
- data/examples/regression_test/05_parallel_explicit.txt +80 -0
- data/examples/regression_test/06_real_world_ecommerce.txt +53 -0
- data/examples/regression_test/07_real_world_etl.txt +58 -0
- data/examples/regression_test/08_graph_visualization.txt +429 -0
- data/examples/regression_test/09_pipeline_visualization.txt +305 -0
- data/examples/regression_test/10_concurrency_control.txt +96 -0
- data/examples/regression_test/11_sequential_dependencies.txt +86 -0
- data/examples/regression_test/12_none_constant.txt +64 -0
- data/examples/regression_test.rb +105 -0
- data/lib/simple_flow/dependency_graph.rb +120 -0
- data/lib/simple_flow/dependency_graph_visualizer.rb +326 -0
- data/lib/simple_flow/middleware.rb +36 -0
- data/lib/simple_flow/parallel_executor.rb +80 -0
- data/lib/simple_flow/pipeline.rb +405 -0
- data/lib/simple_flow/result.rb +88 -0
- data/lib/simple_flow/step_tracker.rb +58 -0
- data/lib/simple_flow/version.rb +5 -0
- data/lib/simple_flow.rb +41 -0
- data/mkdocs.yml +146 -0
- data/pipeline_graph.dot +51 -0
- data/pipeline_graph.html +60 -0
- data/pipeline_graph.mmd +19 -0
- 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
|