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,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
|