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,112 @@
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
+ # Basic pipeline example demonstrating sequential step execution
9
+
10
+ puts "=" * 60
11
+ puts "Basic Pipeline Example"
12
+ puts "=" * 60
13
+ puts
14
+
15
+ # Example 1: Simple data transformation pipeline
16
+ puts "Example 1: Data Transformation Pipeline"
17
+ puts "-" * 60
18
+
19
+ pipeline = SimpleFlow::Pipeline.new do
20
+ step ->(result) {
21
+ puts " Step 1: Trimming whitespace"
22
+ result.continue(result.value.strip)
23
+ }
24
+
25
+ step ->(result) {
26
+ puts " Step 2: Converting to uppercase"
27
+ result.continue(result.value.upcase)
28
+ }
29
+
30
+ step ->(result) {
31
+ puts " Step 3: Adding greeting"
32
+ result.continue("Hello, #{result.value}!")
33
+ }
34
+ end
35
+
36
+ initial_result = SimpleFlow::Result.new(" world ")
37
+ final_result = pipeline.call(initial_result)
38
+
39
+ puts "Input: '#{initial_result.value}'"
40
+ puts "Output: '#{final_result.value}'"
41
+ puts
42
+
43
+ # Example 2: Numerical computation pipeline
44
+ puts "\nExample 2: Numerical Computation Pipeline"
45
+ puts "-" * 60
46
+
47
+ computation_pipeline = SimpleFlow::Pipeline.new do
48
+ step ->(result) {
49
+ puts " Step 1: Add 10"
50
+ result.continue(result.value + 10)
51
+ }
52
+
53
+ step ->(result) {
54
+ puts " Step 2: Multiply by 2"
55
+ result.continue(result.value * 2)
56
+ }
57
+
58
+ step ->(result) {
59
+ puts " Step 3: Subtract 5"
60
+ result.continue(result.value - 5)
61
+ }
62
+ end
63
+
64
+ initial_value = SimpleFlow::Result.new(5)
65
+ computed_result = computation_pipeline.call(initial_value)
66
+
67
+ puts "Input: #{initial_value.value}"
68
+ puts "Output: #{computed_result.value}"
69
+ puts "Formula: (5 + 10) * 2 - 5 = #{computed_result.value}"
70
+ puts
71
+
72
+ # Example 3: Context propagation
73
+ puts "\nExample 3: Context Propagation"
74
+ puts "-" * 60
75
+
76
+ context_pipeline = SimpleFlow::Pipeline.new do
77
+ step ->(result) {
78
+ puts " Step 1: Recording start time"
79
+ result
80
+ .with_context(:started_at, Time.now)
81
+ .continue(result.value)
82
+ }
83
+
84
+ step ->(result) {
85
+ puts " Step 2: Processing data"
86
+ sleep 0.1 # Simulate processing
87
+ result
88
+ .with_context(:processed_at, Time.now)
89
+ .continue(result.value.upcase)
90
+ }
91
+
92
+ step ->(result) {
93
+ puts " Step 3: Recording completion"
94
+ result
95
+ .with_context(:completed_at, Time.now)
96
+ .with_context(:steps_executed, 3)
97
+ .continue(result.value)
98
+ }
99
+ end
100
+
101
+ context_result = context_pipeline.call(SimpleFlow::Result.new("processing"))
102
+
103
+ puts "Result: #{context_result.value}"
104
+ puts "Context:"
105
+ context_result.context.each do |key, value|
106
+ puts " #{key}: #{value}"
107
+ end
108
+ puts
109
+
110
+ puts "=" * 60
111
+ puts "Basic pipeline examples completed!"
112
+ puts "=" * 60
@@ -0,0 +1,178 @@
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
+ # Error handling and flow control examples
9
+
10
+ puts "=" * 60
11
+ puts "Error Handling and Flow Control"
12
+ puts "=" * 60
13
+ puts
14
+
15
+ # Example 1: Validation with halt
16
+ puts "Example 1: Input Validation with Halt"
17
+ puts "-" * 60
18
+
19
+ validation_pipeline = SimpleFlow::Pipeline.new do
20
+ step ->(result) {
21
+ puts " Step 1: Validating age is numeric"
22
+ unless result.value.is_a?(Integer)
23
+ return result.halt.with_error(:validation, "Age must be a number")
24
+ end
25
+ result.continue(result.value)
26
+ }
27
+
28
+ step ->(result) {
29
+ puts " Step 2: Validating age is positive"
30
+ if result.value < 0
31
+ return result.halt.with_error(:validation, "Age cannot be negative")
32
+ end
33
+ result.continue(result.value)
34
+ }
35
+
36
+ step ->(result) {
37
+ puts " Step 3: Checking minimum age"
38
+ if result.value < 18
39
+ return result.halt.with_error(:validation, "Must be 18 or older")
40
+ end
41
+ result.continue(result.value)
42
+ }
43
+
44
+ step ->(result) {
45
+ puts " Step 4: Processing valid age"
46
+ result.continue("Approved for age #{result.value}")
47
+ }
48
+ end
49
+
50
+ # Test with valid age
51
+ puts "\nTest 1: Valid age (21)"
52
+ result1 = validation_pipeline.call(SimpleFlow::Result.new(21))
53
+ puts "Continue? #{result1.continue?}"
54
+ puts "Result: #{result1.value}"
55
+ puts "Errors: #{result1.errors}"
56
+
57
+ # Test with invalid age (under 18)
58
+ puts "\nTest 2: Invalid age (15)"
59
+ result2 = validation_pipeline.call(SimpleFlow::Result.new(15))
60
+ puts "Continue? #{result2.continue?}"
61
+ puts "Result: #{result2.value}"
62
+ puts "Errors: #{result2.errors}"
63
+
64
+ # Test with negative age
65
+ puts "\nTest 3: Negative age (-5)"
66
+ result3 = validation_pipeline.call(SimpleFlow::Result.new(-5))
67
+ puts "Continue? #{result3.continue?}"
68
+ puts "Result: #{result3.value}"
69
+ puts "Errors: #{result3.errors}"
70
+ puts
71
+
72
+ # Example 2: Error accumulation
73
+ puts "\nExample 2: Error Accumulation"
74
+ puts "-" * 60
75
+
76
+ error_accumulation_pipeline = SimpleFlow::Pipeline.new do
77
+ step ->(result) {
78
+ puts " Step 1: Checking password length"
79
+ if result.value[:password].length < 8
80
+ result = result.with_error(:password, "Password must be at least 8 characters")
81
+ end
82
+ result.continue(result.value)
83
+ }
84
+
85
+ step ->(result) {
86
+ puts " Step 2: Checking for uppercase letters"
87
+ unless result.value[:password] =~ /[A-Z]/
88
+ result = result.with_error(:password, "Password must contain uppercase letters")
89
+ end
90
+ result.continue(result.value)
91
+ }
92
+
93
+ step ->(result) {
94
+ puts " Step 3: Checking for numbers"
95
+ unless result.value[:password] =~ /[0-9]/
96
+ result = result.with_error(:password, "Password must contain numbers")
97
+ end
98
+ result.continue(result.value)
99
+ }
100
+
101
+ step ->(result) {
102
+ puts " Step 4: Final validation"
103
+ if result.errors.any?
104
+ result.halt(result.value)
105
+ else
106
+ result.continue({ username: result.value[:username], status: "valid" })
107
+ end
108
+ }
109
+
110
+ step ->(result) {
111
+ puts " Step 5: Creating account (only runs if valid)"
112
+ result.continue("Account created for #{result.value[:username]}")
113
+ }
114
+ end
115
+
116
+ # Test with weak password
117
+ puts "\nTest 1: Weak password"
118
+ weak_password_result = error_accumulation_pipeline.call(
119
+ SimpleFlow::Result.new({ username: "john", password: "weak" })
120
+ )
121
+ puts "Continue? #{weak_password_result.continue?}"
122
+ puts "Result: #{weak_password_result.value}"
123
+ puts "Errors: #{weak_password_result.errors}"
124
+
125
+ # Test with strong password
126
+ puts "\nTest 2: Strong password"
127
+ strong_password_result = error_accumulation_pipeline.call(
128
+ SimpleFlow::Result.new({ username: "jane", password: "SecurePass123" })
129
+ )
130
+ puts "Continue? #{strong_password_result.continue?}"
131
+ puts "Result: #{strong_password_result.value}"
132
+ puts "Errors: #{strong_password_result.errors}"
133
+ puts
134
+
135
+ # Example 3: Conditional branching
136
+ puts "\nExample 3: Conditional Processing"
137
+ puts "-" * 60
138
+
139
+ conditional_pipeline = SimpleFlow::Pipeline.new do
140
+ step ->(result) {
141
+ puts " Step 1: Checking user role"
142
+ result.with_context(:user_role, result.value[:role]).continue(result.value)
143
+ }
144
+
145
+ step ->(result) {
146
+ puts " Step 2: Role-based processing"
147
+ case result.context[:user_role]
148
+ when :admin
149
+ result.with_context(:permissions, [:read, :write, :delete]).continue(result.value)
150
+ when :editor
151
+ result.with_context(:permissions, [:read, :write]).continue(result.value)
152
+ when :viewer
153
+ result.with_context(:permissions, [:read]).continue(result.value)
154
+ else
155
+ result.halt.with_error(:auth, "Unknown role: #{result.context[:user_role]}")
156
+ end
157
+ }
158
+
159
+ step ->(result) {
160
+ puts " Step 3: Generating access token"
161
+ permissions = result.context[:permissions]
162
+ result.continue("Token granted with permissions: #{permissions.join(', ')}")
163
+ }
164
+ end
165
+
166
+ # Test different roles
167
+ [:admin, :editor, :viewer, :unknown].each do |role|
168
+ puts "\nTesting role: #{role}"
169
+ result = conditional_pipeline.call(SimpleFlow::Result.new({ role: role }))
170
+ puts " Continue? #{result.continue?}"
171
+ puts " Result: #{result.value}"
172
+ puts " Errors: #{result.errors}" if result.errors.any?
173
+ puts " Permissions: #{result.context[:permissions]}" if result.context[:permissions]
174
+ end
175
+
176
+ puts "\n" + "=" * 60
177
+ puts "Error handling examples completed!"
178
+ puts "=" * 60
@@ -0,0 +1,186 @@
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
+ # Middleware examples showing logging, instrumentation, and custom middleware
9
+
10
+ puts "=" * 60
11
+ puts "Middleware Examples"
12
+ puts "=" * 60
13
+ puts
14
+
15
+ # Example 1: Logging middleware
16
+ puts "Example 1: Logging Middleware"
17
+ puts "-" * 60
18
+
19
+ pipeline_with_logging = SimpleFlow::Pipeline.new do
20
+ use_middleware SimpleFlow::MiddleWare::Logging
21
+
22
+ step ->(result) {
23
+ result.continue(result.value * 2)
24
+ }
25
+
26
+ step ->(result) {
27
+ result.continue(result.value + 10)
28
+ }
29
+ end
30
+
31
+ puts "Executing pipeline with logging middleware:"
32
+ result1 = pipeline_with_logging.call(SimpleFlow::Result.new(5))
33
+ puts "Final result: #{result1.value}"
34
+ puts
35
+
36
+ # Example 2: Instrumentation middleware
37
+ puts "\nExample 2: Instrumentation Middleware"
38
+ puts "-" * 60
39
+
40
+ pipeline_with_instrumentation = SimpleFlow::Pipeline.new do
41
+ use_middleware SimpleFlow::MiddleWare::Instrumentation, api_key: 'demo-key-123'
42
+
43
+ step ->(result) {
44
+ sleep 0.01 # Simulate work
45
+ result.continue(result.value.upcase)
46
+ }
47
+
48
+ step ->(result) {
49
+ sleep 0.02 # Simulate more work
50
+ result.continue("Processed: #{result.value}")
51
+ }
52
+ end
53
+
54
+ puts "Executing pipeline with instrumentation middleware:"
55
+ result2 = pipeline_with_instrumentation.call(SimpleFlow::Result.new("data"))
56
+ puts "Final result: #{result2.value}"
57
+ puts
58
+
59
+ # Example 3: Multiple middleware (stacked)
60
+ puts "\nExample 3: Stacked Middleware"
61
+ puts "-" * 60
62
+
63
+ pipeline_with_multiple = SimpleFlow::Pipeline.new do
64
+ use_middleware SimpleFlow::MiddleWare::Instrumentation, api_key: 'stacked-demo'
65
+ use_middleware SimpleFlow::MiddleWare::Logging
66
+
67
+ step ->(result) {
68
+ result.continue(result.value + 5)
69
+ }
70
+
71
+ step ->(result) {
72
+ result.continue(result.value * 3)
73
+ }
74
+ end
75
+
76
+ puts "Executing pipeline with multiple middleware:"
77
+ result3 = pipeline_with_multiple.call(SimpleFlow::Result.new(10))
78
+ puts "Final result: #{result3.value}"
79
+ puts
80
+
81
+ # Example 4: Custom middleware - retry logic
82
+ puts "\nExample 4: Custom Retry Middleware"
83
+ puts "-" * 60
84
+
85
+ class RetryMiddleware
86
+ def initialize(callable, max_retries: 3)
87
+ @callable = callable
88
+ @max_retries = max_retries
89
+ end
90
+
91
+ def call(result)
92
+ attempts = 0
93
+ begin
94
+ attempts += 1
95
+ puts " Attempt #{attempts} of #{@max_retries}"
96
+ @callable.call(result)
97
+ rescue StandardError => e
98
+ if attempts < @max_retries
99
+ puts " Failed (#{e.message}), retrying..."
100
+ retry
101
+ else
102
+ puts " Failed after #{@max_retries} attempts"
103
+ result.halt.with_error(:retry, "Failed after #{@max_retries} attempts: #{e.message}")
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ # Simulated flaky operation
110
+ attempt_count = 0
111
+ flaky_operation = ->(result) {
112
+ attempt_count += 1
113
+ if attempt_count < 2
114
+ raise StandardError, "Temporary failure"
115
+ end
116
+ result.continue("Success on attempt #{attempt_count}")
117
+ }
118
+
119
+ retry_pipeline = SimpleFlow::Pipeline.new do
120
+ use_middleware RetryMiddleware, max_retries: 3
121
+ step flaky_operation
122
+ end
123
+
124
+ puts "Executing pipeline with retry middleware:"
125
+ result4 = retry_pipeline.call(SimpleFlow::Result.new(nil))
126
+ puts "Final result: #{result4.value}"
127
+ puts
128
+
129
+ # Example 5: Custom middleware - authentication
130
+ puts "\nExample 5: Custom Authentication Middleware"
131
+ puts "-" * 60
132
+
133
+ class AuthMiddleware
134
+ def initialize(callable, required_role:)
135
+ @callable = callable
136
+ @required_role = required_role
137
+ end
138
+
139
+ def call(result)
140
+ user_role = result.context[:user_role]
141
+
142
+ unless user_role == @required_role
143
+ puts " Access denied: requires #{@required_role}, got #{user_role}"
144
+ return result.halt.with_error(:auth, "Unauthorized: requires #{@required_role} role")
145
+ end
146
+
147
+ puts " Access granted for #{@required_role}"
148
+ @callable.call(result)
149
+ end
150
+ end
151
+
152
+ auth_pipeline = SimpleFlow::Pipeline.new do
153
+ # First step sets the user role in context
154
+ step ->(result) {
155
+ result.with_context(:user_role, result.value[:role]).continue(result.value)
156
+ }
157
+
158
+ # Protected step - requires admin role
159
+ use_middleware AuthMiddleware, required_role: :admin
160
+
161
+ step ->(result) {
162
+ result.continue("Sensitive admin operation completed")
163
+ }
164
+ end
165
+
166
+ # Test with admin role
167
+ puts "\nTest 1: Admin user"
168
+ admin_result = auth_pipeline.call(
169
+ SimpleFlow::Result.new({ role: :admin })
170
+ )
171
+ puts "Continue? #{admin_result.continue?}"
172
+ puts "Result: #{admin_result.value}"
173
+ puts "Errors: #{admin_result.errors}"
174
+
175
+ # Test with regular user
176
+ puts "\nTest 2: Regular user"
177
+ user_result = auth_pipeline.call(
178
+ SimpleFlow::Result.new({ role: :user })
179
+ )
180
+ puts "Continue? #{user_result.continue?}"
181
+ puts "Result: #{user_result.value}"
182
+ puts "Errors: #{user_result.errors}"
183
+
184
+ puts "\n" + "=" * 60
185
+ puts "Middleware examples completed!"
186
+ puts "=" * 60
@@ -0,0 +1,221 @@
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
+ # Automatic parallel discovery using dependency graphs
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 "Automatic Parallel Discovery"
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 discovery
31
+ puts "Example 1: Basic Parallel Execution"
32
+ puts "-" * 60
33
+ puts
34
+
35
+ pipeline = SimpleFlow::Pipeline.new do
36
+ # This step has no dependencies - runs first
37
+ step :fetch_user, ->(result) {
38
+ puts " [#{Time.now.strftime('%H:%M:%S.%L')}] Fetching user..."
39
+ sleep 0.1 # Simulate API call
40
+ result.with_context(:user, { id: result.value, name: "John Doe" }).continue(result.value)
41
+ }, depends_on: :none
42
+
43
+ # These two steps both depend on :fetch_user, so they can run in parallel
44
+ step :fetch_orders, ->(result) {
45
+ puts " [#{Time.now.strftime('%H:%M:%S.%L')}] Fetching orders..."
46
+ sleep 0.1 # Simulate API call
47
+ result.with_context(:orders, [1, 2, 3]).continue(result.value)
48
+ }, depends_on: [:fetch_user]
49
+
50
+ step :fetch_products, ->(result) {
51
+ puts " [#{Time.now.strftime('%H:%M:%S.%L')}] Fetching products..."
52
+ sleep 0.1 # Simulate API call
53
+ result.with_context(:products, [:a, :b, :c]).continue(result.value)
54
+ }, depends_on: [:fetch_user]
55
+
56
+ # This step depends on both parallel steps - runs last
57
+ step :calculate_total, ->(result) {
58
+ puts " [#{Time.now.strftime('%H:%M:%S.%L')}] Calculating total..."
59
+ orders = result.context[:orders]
60
+ products = result.context[:products]
61
+ result.continue("Total: #{orders.size} orders, #{products.size} products")
62
+ }, depends_on: [:fetch_orders, :fetch_products]
63
+ end
64
+
65
+ start_time = Time.now
66
+ result = pipeline.call_parallel(SimpleFlow::Result.new(123))
67
+ elapsed = Time.now - start_time
68
+
69
+ puts "\nResult: #{result.value}"
70
+ puts "User: #{result.context[:user]}"
71
+ puts "Orders: #{result.context[:orders]}"
72
+ puts "Products: #{result.context[:products]}"
73
+ puts "Execution time: #{(elapsed * 1000).round(2)}ms"
74
+ puts "(Should be ~200ms with parallel, ~400ms sequential)"
75
+ puts
76
+
77
+ # Example 2: Complex dependency graph
78
+ puts "\nExample 2: Complex Dependency Graph"
79
+ puts "-" * 60
80
+ puts
81
+
82
+ complex_pipeline = SimpleFlow::Pipeline.new do
83
+ # Level 1: No dependencies
84
+ step :validate_input, ->(result) {
85
+ puts " [Level 1] Validating input..."
86
+ sleep 0.05
87
+ result.with_context(:validated, true).continue(result.value)
88
+ }, depends_on: :none
89
+
90
+ # Level 2: Depends on validation (can run in parallel with each other)
91
+ step :check_inventory, ->(result) {
92
+ puts " [Level 2] Checking inventory..."
93
+ sleep 0.05
94
+ result.with_context(:inventory, :available).continue(result.value)
95
+ }, depends_on: [:validate_input]
96
+
97
+ step :check_pricing, ->(result) {
98
+ puts " [Level 2] Checking pricing..."
99
+ sleep 0.05
100
+ result.with_context(:price, 100).continue(result.value)
101
+ }, depends_on: [:validate_input]
102
+
103
+ step :check_shipping, ->(result) {
104
+ puts " [Level 2] Checking shipping..."
105
+ sleep 0.05
106
+ result.with_context(:shipping, 10).continue(result.value)
107
+ }, depends_on: [:validate_input]
108
+
109
+ # Level 3: Depends on inventory and pricing (runs after level 2)
110
+ step :calculate_discount, ->(result) {
111
+ puts " [Level 3] Calculating discount..."
112
+ sleep 0.05
113
+ price = result.context[:price]
114
+ result.with_context(:discount, price * 0.1).continue(result.value)
115
+ }, depends_on: [:check_inventory, :check_pricing]
116
+
117
+ # Level 4: Final step (depends on everything)
118
+ step :finalize_order, ->(result) {
119
+ puts " [Level 4] Finalizing order..."
120
+ price = result.context[:price]
121
+ shipping = result.context[:shipping]
122
+ discount = result.context[:discount]
123
+ total = price + shipping - discount
124
+ result.continue("Order total: $#{total}")
125
+ }, depends_on: [:calculate_discount, :check_shipping]
126
+ end
127
+
128
+ puts "Dependency graph structure:"
129
+ puts " Level 1: validate_input"
130
+ puts " Level 2: check_inventory, check_pricing, check_shipping (parallel)"
131
+ puts " Level 3: calculate_discount"
132
+ puts " Level 4: finalize_order"
133
+ puts
134
+
135
+ start_time = Time.now
136
+ result2 = complex_pipeline.call_parallel(SimpleFlow::Result.new({ product_id: 456 }))
137
+ elapsed2 = Time.now - start_time
138
+
139
+ puts "\nResult: #{result2.value}"
140
+ puts "Context: #{result2.context}"
141
+ puts "Execution time: #{(elapsed2 * 1000).round(2)}ms"
142
+ puts
143
+
144
+ # Example 3: Visualizing the dependency graph
145
+ puts "\nExample 3: Dependency Graph Analysis"
146
+ puts "-" * 60
147
+ puts
148
+
149
+ # Create a graph manually to show analysis
150
+ graph = SimpleFlow::DependencyGraph.new(
151
+ fetch_user: [],
152
+ fetch_orders: [:fetch_user],
153
+ fetch_products: [:fetch_user],
154
+ fetch_reviews: [:fetch_user],
155
+ calculate_stats: [:fetch_orders, :fetch_products],
156
+ generate_report: [:calculate_stats, :fetch_reviews]
157
+ )
158
+
159
+ puts "Dependencies:"
160
+ graph.dependencies.each do |step, deps|
161
+ deps_str = deps.empty? ? "(none)" : deps.join(", ")
162
+ puts " #{step}: #{deps_str}"
163
+ end
164
+
165
+ puts "\nSequential order:"
166
+ puts " #{graph.order.join(' → ')}"
167
+
168
+ puts "\nParallel execution groups:"
169
+ graph.parallel_order.each_with_index do |group, index|
170
+ puts " Group #{index + 1}: #{group.join(', ')}"
171
+ end
172
+
173
+ puts "\nExecution strategy:"
174
+ puts " • fetch_user runs first (no dependencies)"
175
+ puts " • fetch_orders, fetch_products, fetch_reviews run in parallel"
176
+ puts " • calculate_stats waits for orders and products"
177
+ puts " • generate_report waits for stats and reviews"
178
+ puts
179
+
180
+ # Example 4: Error handling in parallel steps
181
+ puts "\nExample 4: Error Handling in Parallel Execution"
182
+ puts "-" * 60
183
+ puts
184
+
185
+ error_pipeline = SimpleFlow::Pipeline.new do
186
+ step :task_a, ->(result) {
187
+ puts " Task A: Processing..."
188
+ sleep 0.05
189
+ result.with_context(:task_a, :success).continue(result.value)
190
+ }, depends_on: :none
191
+
192
+ step :task_b, ->(result) {
193
+ puts " Task B: Processing..."
194
+ sleep 0.05
195
+ # Simulate a failure
196
+ result.halt.with_error(:task_b, "Task B encountered an error")
197
+ }, depends_on: :none
198
+
199
+ step :task_c, ->(result) {
200
+ puts " Task C: Processing..."
201
+ sleep 0.05
202
+ result.with_context(:task_c, :success).continue(result.value)
203
+ }, depends_on: :none
204
+
205
+ step :final_step, ->(result) {
206
+ puts " Final step: This should not execute"
207
+ result.continue("Completed")
208
+ }, depends_on: [:task_a, :task_b, :task_c]
209
+ end
210
+
211
+ result3 = error_pipeline.call_parallel(SimpleFlow::Result.new(nil))
212
+
213
+ puts "\nResult:"
214
+ puts " Continue? #{result3.continue?}"
215
+ puts " Errors: #{result3.errors}"
216
+ puts " Context: #{result3.context}"
217
+ puts " Note: Pipeline halted when task_b failed, preventing final_step"
218
+
219
+ puts "\n" + "=" * 60
220
+ puts "Automatic parallel discovery examples completed!"
221
+ puts "=" * 60