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,440 @@
1
+ # Complex Workflows Guide
2
+
3
+ This guide demonstrates how to build sophisticated, real-world workflows using SimpleFlow's advanced features.
4
+
5
+ ## E-Commerce Order Processing
6
+
7
+ Complete order processing pipeline with validation, inventory, payment, and notifications:
8
+
9
+ ```ruby
10
+ class OrderProcessor
11
+ def self.build
12
+ SimpleFlow::Pipeline.new do
13
+ # Step 1: Validate order
14
+ step :validate_order, ->(result) {
15
+ order = result.value
16
+ errors = []
17
+
18
+ errors << "Missing email" unless order[:customer][:email]
19
+ errors << "No items" if order[:items].empty?
20
+ errors << "Missing payment" unless order[:payment][:card_token]
21
+
22
+ if errors.any?
23
+ result.halt.with_error(:validation, errors.join(", "))
24
+ else
25
+ result.with_context(:validated_at, Time.now).continue(order)
26
+ end
27
+ }, depends_on: []
28
+
29
+ # Steps 2-3: Parallel checks
30
+ step :check_inventory, ->(result) {
31
+ order = result.value
32
+ inventory_results = order[:items].map do |item|
33
+ InventoryService.check_availability(item[:product_id])
34
+ end
35
+
36
+ if inventory_results.all? { |r| r[:available] }
37
+ result.with_context(:inventory_check, inventory_results).continue(order)
38
+ else
39
+ result.halt.with_error(:inventory, "Items out of stock")
40
+ end
41
+ }, depends_on: [:validate_order]
42
+
43
+ step :calculate_shipping, ->(result) {
44
+ order = result.value
45
+ shipping = ShippingService.calculate(
46
+ order[:shipping_address],
47
+ order[:items]
48
+ )
49
+ result.with_context(:shipping, shipping).continue(order)
50
+ }, depends_on: [:validate_order]
51
+
52
+ # Step 4: Calculate totals
53
+ step :calculate_totals, ->(result) {
54
+ order = result.value
55
+ shipping = result.context[:shipping]
56
+
57
+ subtotal = order[:items].sum { |item| item[:price] * item[:quantity] }
58
+ tax = subtotal * 0.08
59
+ total = subtotal + tax + shipping[:cost]
60
+
61
+ result
62
+ .with_context(:subtotal, subtotal)
63
+ .with_context(:tax, tax)
64
+ .with_context(:total, total)
65
+ .continue(order)
66
+ }, depends_on: [:check_inventory, :calculate_shipping]
67
+
68
+ # Step 5: Process payment
69
+ step :process_payment, ->(result) {
70
+ order = result.value
71
+ total = result.context[:total]
72
+
73
+ payment_result = PaymentService.process(
74
+ total,
75
+ order[:payment][:card_token]
76
+ )
77
+
78
+ if payment_result[:status] == :success
79
+ result.with_context(:payment, payment_result).continue(order)
80
+ else
81
+ result.halt.with_error(:payment, payment_result[:reason])
82
+ end
83
+ }, depends_on: [:calculate_totals]
84
+
85
+ # Step 6: Reserve inventory
86
+ step :reserve_inventory, ->(result) {
87
+ order = result.value
88
+ reservation = InventoryService.reserve(order[:items])
89
+ result.with_context(:reservation, reservation).continue(order)
90
+ }, depends_on: [:process_payment]
91
+
92
+ # Step 7: Create shipment
93
+ step :create_shipment, ->(result) {
94
+ order = result.value
95
+ shipment = ShippingService.create_shipment(
96
+ order[:order_id],
97
+ order[:shipping_address]
98
+ )
99
+ result.with_context(:shipment, shipment).continue(order)
100
+ }, depends_on: [:reserve_inventory]
101
+
102
+ # Steps 8-9: Parallel notifications
103
+ step :send_email, ->(result) {
104
+ order = result.value
105
+ NotificationService.send_email(
106
+ order[:customer][:email],
107
+ "Order Confirmed",
108
+ order_confirmation_body(order, result.context)
109
+ )
110
+ result.continue(order)
111
+ }, depends_on: [:create_shipment]
112
+
113
+ step :send_sms, ->(result) {
114
+ order = result.value
115
+ if order[:customer][:phone]
116
+ NotificationService.send_sms(
117
+ order[:customer][:phone],
118
+ "Order confirmed! Tracking: #{result.context[:shipment][:tracking]}"
119
+ )
120
+ end
121
+ result.continue(order)
122
+ }, depends_on: [:create_shipment]
123
+
124
+ # Step 10: Finalize
125
+ step :finalize_order, ->(result) {
126
+ order = result.value
127
+ final_order = {
128
+ order_id: order[:order_id],
129
+ status: :confirmed,
130
+ total: result.context[:total],
131
+ payment_transaction: result.context[:payment][:transaction_id],
132
+ tracking_number: result.context[:shipment][:tracking_number]
133
+ }
134
+ result.continue(final_order)
135
+ }, depends_on: [:send_email, :send_sms]
136
+ end
137
+ end
138
+ end
139
+
140
+ # Usage
141
+ result = OrderProcessor.build.call_parallel(
142
+ SimpleFlow::Result.new(order_data)
143
+ )
144
+
145
+ if result.continue?
146
+ puts "Order #{result.value[:order_id]} processed successfully"
147
+ else
148
+ puts "Order failed: #{result.errors}"
149
+ end
150
+ ```
151
+
152
+ ## ETL Data Pipeline
153
+
154
+ Extract, Transform, Load pipeline with validation and error handling:
155
+
156
+ ```ruby
157
+ class ETLPipeline
158
+ def self.build
159
+ SimpleFlow::Pipeline.new do
160
+ # Extract phase - parallel loading
161
+ step :extract_users, ->(result) {
162
+ users = DataSource.fetch_users_csv
163
+ result.with_context(:raw_users, users).continue(result.value)
164
+ }, depends_on: []
165
+
166
+ step :extract_orders, ->(result) {
167
+ orders = DataSource.fetch_orders_json
168
+ result.with_context(:raw_orders, orders).continue(result.value)
169
+ }, depends_on: []
170
+
171
+ step :extract_products, ->(result) {
172
+ products = DataSource.fetch_products_api
173
+ result.with_context(:raw_products, products).continue(result.value)
174
+ }, depends_on: []
175
+
176
+ # Transform phase - parallel transformations
177
+ step :transform_users, ->(result) {
178
+ users = result.context[:raw_users].map do |user|
179
+ {
180
+ id: user[:id],
181
+ name: user[:name].downcase.split.map(&:capitalize).join(' '),
182
+ email: user[:email].downcase,
183
+ signup_year: user[:signup_date].split('-').first.to_i
184
+ }
185
+ end
186
+ result.with_context(:users, users).continue(result.value)
187
+ }, depends_on: [:extract_users]
188
+
189
+ step :transform_orders, ->(result) {
190
+ orders = result.context[:raw_orders]
191
+ .reject { |o| o[:status] == "cancelled" }
192
+ .map do |order|
193
+ {
194
+ id: order[:order_id],
195
+ user_id: order[:user_id],
196
+ amount: order[:amount],
197
+ tax: (order[:amount] * 0.08).round(2),
198
+ total: (order[:amount] * 1.08).round(2)
199
+ }
200
+ end
201
+ result.with_context(:orders, orders).continue(result.value)
202
+ }, depends_on: [:extract_orders]
203
+
204
+ step :transform_products, ->(result) {
205
+ products = result.context[:raw_products].map do |product|
206
+ {
207
+ id: product[:product_id],
208
+ name: product[:name],
209
+ category: product[:category].to_sym
210
+ }
211
+ end
212
+ result.with_context(:products, products).continue(result.value)
213
+ }, depends_on: [:extract_products]
214
+
215
+ # Aggregate phase
216
+ step :aggregate_stats, ->(result) {
217
+ users = result.context[:users]
218
+ orders = result.context[:orders]
219
+
220
+ stats = users.map do |user|
221
+ user_orders = orders.select { |o| o[:user_id] == user[:id] }
222
+ {
223
+ user_id: user[:id],
224
+ name: user[:name],
225
+ total_orders: user_orders.size,
226
+ total_spent: user_orders.sum { |o| o[:total] },
227
+ avg_order: user_orders.empty? ? 0 : user_orders.sum { |o| o[:total] } / user_orders.size
228
+ }
229
+ end
230
+
231
+ result.with_context(:user_stats, stats).continue(result.value)
232
+ }, depends_on: [:transform_users, :transform_orders]
233
+
234
+ # Validation phase
235
+ step :validate_data, ->(result) {
236
+ users = result.context[:users]
237
+ orders = result.context[:orders]
238
+
239
+ issues = []
240
+
241
+ # Check for orphaned orders
242
+ user_ids = users.map { |u| u[:id] }
243
+ orphaned = orders.reject { |o| user_ids.include?(o[:user_id]) }
244
+ issues << "#{orphaned.size} orphaned orders" if orphaned.any?
245
+
246
+ result.with_context(:validation_warnings, issues).continue(result.value)
247
+ }, depends_on: [:aggregate_stats]
248
+
249
+ # Load phase
250
+ step :prepare_output, ->(result) {
251
+ output = {
252
+ metadata: {
253
+ processed_at: Time.now,
254
+ warnings: result.context[:validation_warnings]
255
+ },
256
+ analytics: {
257
+ user_stats: result.context[:user_stats],
258
+ summary: {
259
+ total_users: result.context[:users].size,
260
+ total_orders: result.context[:orders].size,
261
+ total_revenue: result.context[:orders].sum { |o| o[:total] }
262
+ }
263
+ }
264
+ }
265
+ result.continue(output)
266
+ }, depends_on: [:validate_data]
267
+ end
268
+ end
269
+ end
270
+ ```
271
+
272
+ ## Multi-Service Integration
273
+
274
+ Orchestrating multiple external services:
275
+
276
+ ```ruby
277
+ class UserOnboarding
278
+ def self.build
279
+ SimpleFlow::Pipeline.new do
280
+ # Validate user data
281
+ step :validate_user, ->(result) {
282
+ user_data = result.value
283
+ validator = UserValidator.new(user_data)
284
+
285
+ if validator.valid?
286
+ result.continue(user_data)
287
+ else
288
+ result.halt.with_error(:validation, validator.errors.join(", "))
289
+ end
290
+ }, depends_on: []
291
+
292
+ # Parallel service calls
293
+ step :create_auth_account, ->(result) {
294
+ user = result.value
295
+ auth_account = AuthService.create_account(
296
+ email: user[:email],
297
+ password: user[:password]
298
+ )
299
+ result.with_context(:auth_id, auth_account[:id]).continue(user)
300
+ }, depends_on: [:validate_user]
301
+
302
+ step :create_profile, ->(result) {
303
+ user = result.value
304
+ profile = ProfileService.create(
305
+ name: user[:name],
306
+ bio: user[:bio]
307
+ )
308
+ result.with_context(:profile_id, profile[:id]).continue(user)
309
+ }, depends_on: [:validate_user]
310
+
311
+ step :setup_preferences, ->(result) {
312
+ user = result.value
313
+ prefs = PreferenceService.initialize_defaults(user[:preferences] || {})
314
+ result.with_context(:preferences_id, prefs[:id]).continue(user)
315
+ }, depends_on: [:validate_user]
316
+
317
+ # Link accounts
318
+ step :link_accounts, ->(result) {
319
+ user_record = User.create!(
320
+ email: result.value[:email],
321
+ auth_id: result.context[:auth_id],
322
+ profile_id: result.context[:profile_id],
323
+ preferences_id: result.context[:preferences_id]
324
+ )
325
+ result.with_context(:user, user_record).continue(user_record)
326
+ }, depends_on: [:create_auth_account, :create_profile, :setup_preferences]
327
+
328
+ # Parallel post-creation tasks
329
+ step :send_welcome_email, ->(result) {
330
+ user = result.context[:user]
331
+ EmailService.send_welcome(user.email)
332
+ result.continue(result.value)
333
+ }, depends_on: [:link_accounts]
334
+
335
+ step :trigger_analytics, ->(result) {
336
+ user = result.context[:user]
337
+ AnalyticsService.track_signup(user)
338
+ result.continue(result.value)
339
+ }, depends_on: [:link_accounts]
340
+
341
+ step :create_trial_subscription, ->(result) {
342
+ user = result.context[:user]
343
+ subscription = BillingService.create_trial(user)
344
+ result.with_context(:subscription, subscription).continue(result.value)
345
+ }, depends_on: [:link_accounts]
346
+
347
+ # Finalize
348
+ step :finalize, ->(result) {
349
+ {
350
+ user_id: result.context[:user].id,
351
+ subscription_id: result.context[:subscription][:id],
352
+ onboarded_at: Time.now
353
+ }
354
+ }, depends_on: [:send_welcome_email, :trigger_analytics, :create_trial_subscription]
355
+ end
356
+ end
357
+ end
358
+ ```
359
+
360
+ ## Error Recovery Workflow
361
+
362
+ Advanced error handling with fallbacks and retries:
363
+
364
+ ```ruby
365
+ class ResilientDataFetcher
366
+ def self.build
367
+ SimpleFlow::Pipeline.new do
368
+ # Try primary data source
369
+ step :fetch_primary, ->(result) {
370
+ begin
371
+ data = PrimaryAPI.fetch(result.value)
372
+ result.with_context(:source, :primary).continue(data)
373
+ rescue PrimaryAPI::Error => e
374
+ result
375
+ .with_context(:primary_error, e.message)
376
+ .continue(result.value)
377
+ end
378
+ }, depends_on: []
379
+
380
+ # Try secondary if primary failed
381
+ step :fetch_secondary, ->(result) {
382
+ # Skip if primary succeeded
383
+ if result.context[:source] == :primary
384
+ return result.continue(result.value)
385
+ end
386
+
387
+ begin
388
+ data = SecondaryAPI.fetch(result.value)
389
+ result.with_context(:source, :secondary).continue(data)
390
+ rescue SecondaryAPI::Error => e
391
+ result
392
+ .with_context(:secondary_error, e.message)
393
+ .continue(result.value)
394
+ end
395
+ }, depends_on: [:fetch_primary]
396
+
397
+ # Fallback to cache
398
+ step :fetch_cache, ->(result) {
399
+ # Skip if we have data
400
+ if result.context[:source]
401
+ return result.continue(result.value)
402
+ end
403
+
404
+ cached = CacheService.get(result.value)
405
+ if cached
406
+ result.with_context(:source, :cache).continue(cached)
407
+ else
408
+ result.halt.with_error(
409
+ :data_unavailable,
410
+ "All data sources failed: #{[
411
+ result.context[:primary_error],
412
+ result.context[:secondary_error]
413
+ ].compact.join(', ')}"
414
+ )
415
+ end
416
+ }, depends_on: [:fetch_secondary]
417
+
418
+ # Update cache if we fetched from API
419
+ step :update_cache, ->(result) {
420
+ if [:primary, :secondary].include?(result.context[:source])
421
+ CacheService.set(result.value, result.value)
422
+ end
423
+ result.continue(result.value)
424
+ }, depends_on: [:fetch_cache]
425
+ end
426
+ end
427
+ end
428
+ ```
429
+
430
+ ## For complete examples, see:
431
+
432
+ - `/Users/dewayne/sandbox/git_repos/madbomber/simple_flow/examples/06_real_world_ecommerce.rb` - Full e-commerce workflow
433
+ - `/Users/dewayne/sandbox/git_repos/madbomber/simple_flow/examples/07_real_world_etl.rb` - Complete ETL pipeline
434
+
435
+ ## Related Documentation
436
+
437
+ - [Error Handling](error-handling.md) - Error handling strategies
438
+ - [Validation Patterns](validation-patterns.md) - Data validation
439
+ - [Data Fetching](data-fetching.md) - Fetching external data
440
+ - [Parallel Steps](../concurrent/parallel-steps.md) - Concurrent execution