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,243 @@
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
+ # Sequential Step Dependencies Example
9
+ #
10
+ # This example demonstrates how unnamed steps automatically depend on the
11
+ # previous step's success, and how the pipeline short-circuits when a step halts.
12
+
13
+ puts "=" * 60
14
+ puts "Sequential Step Dependencies"
15
+ puts "=" * 60
16
+ puts
17
+
18
+ # Example 1: Successful sequential execution
19
+ puts "Example 1: All Steps Succeed"
20
+ puts "-" * 60
21
+ puts
22
+
23
+ successful_pipeline = SimpleFlow::Pipeline.new do
24
+ step ->(result) {
25
+ puts " [Step 1] Starting workflow..."
26
+ result.continue(result.value + 1)
27
+ }
28
+
29
+ step ->(result) {
30
+ puts " [Step 2] Processing data..."
31
+ result.continue(result.value * 2)
32
+ }
33
+
34
+ step ->(result) {
35
+ puts " [Step 3] Finalizing..."
36
+ result.continue(result.value + 10)
37
+ }
38
+ end
39
+
40
+ result1 = successful_pipeline.call(SimpleFlow::Result.new(5))
41
+ puts "\nFinal result: #{result1.value}"
42
+ puts "Continue? #{result1.continue?}"
43
+ puts "Expected: ((5 + 1) * 2) + 10 = 22"
44
+ puts
45
+
46
+ # Example 2: Pipeline halts in middle step
47
+ puts "\nExample 2: Pipeline Halts on Error"
48
+ puts "-" * 60
49
+ puts
50
+
51
+ halting_pipeline = SimpleFlow::Pipeline.new do
52
+ step ->(result) {
53
+ puts " [Step 1] Validating input..."
54
+ if result.value.nil?
55
+ return result.halt.with_error(:validation, "Input cannot be nil")
56
+ end
57
+ result.continue(result.value)
58
+ }
59
+
60
+ step ->(result) {
61
+ puts " [Step 2] Checking business rules..."
62
+ if result.value < 0
63
+ return result.halt.with_error(:business_rule, "Value must be positive")
64
+ end
65
+ result.continue(result.value)
66
+ }
67
+
68
+ step ->(result) {
69
+ puts " [Step 3] This step should NOT execute"
70
+ result.continue(result.value * 100)
71
+ }
72
+ end
73
+
74
+ result2 = halting_pipeline.call(SimpleFlow::Result.new(-5))
75
+ puts "\nFinal result: #{result2.value}"
76
+ puts "Continue? #{result2.continue?}"
77
+ puts "Errors: #{result2.errors}"
78
+ puts "Note: Step 3 never executed because Step 2 halted"
79
+ puts
80
+
81
+ # Example 3: Early validation pattern
82
+ puts "\nExample 3: Early Validation Pattern"
83
+ puts "-" * 60
84
+ puts
85
+
86
+ validation_pipeline = SimpleFlow::Pipeline.new do
87
+ step ->(result) {
88
+ puts " [Pre-flight] Checking system health..."
89
+ unless result.context[:system_healthy]
90
+ return result
91
+ .halt("System maintenance in progress")
92
+ .with_error(:system, "Maintenance mode active")
93
+ end
94
+ puts " [Pre-flight] System healthy, proceeding..."
95
+ result.continue(result.value)
96
+ }
97
+
98
+ step ->(result) {
99
+ puts " [Step 1] Processing order..."
100
+ result.continue(result.value)
101
+ }
102
+
103
+ step ->(result) {
104
+ puts " [Step 2] Charging payment..."
105
+ result.continue(result.value)
106
+ }
107
+
108
+ step ->(result) {
109
+ puts " [Step 3] Sending confirmation..."
110
+ result.continue(result.value)
111
+ }
112
+ end
113
+
114
+ # Test with unhealthy system
115
+ puts "Scenario A: System in maintenance mode"
116
+ result3a = validation_pipeline.call(
117
+ SimpleFlow::Result.new({ order_id: 123 })
118
+ .with_context(:system_healthy, false)
119
+ )
120
+ puts "Continue? #{result3a.continue?}"
121
+ puts "Errors: #{result3a.errors}"
122
+ puts
123
+
124
+ # Test with healthy system
125
+ puts "\nScenario B: System healthy"
126
+ result3b = validation_pipeline.call(
127
+ SimpleFlow::Result.new({ order_id: 456 })
128
+ .with_context(:system_healthy, true)
129
+ )
130
+ puts "Continue? #{result3b.continue?}"
131
+ puts "All steps executed successfully!"
132
+ puts
133
+
134
+ # Example 4: Multi-step validation with error accumulation
135
+ puts "\nExample 4: Error Accumulation Before Halting"
136
+ puts "-" * 60
137
+ puts
138
+
139
+ multi_validation_pipeline = SimpleFlow::Pipeline.new do
140
+ step ->(result) {
141
+ puts " [Step 1] Collecting validation errors..."
142
+ data = result.value
143
+ result_with_errors = result
144
+
145
+ # Accumulate all validation errors
146
+ if data[:email].nil? || data[:email].empty?
147
+ result_with_errors = result_with_errors.with_error(:validation, "Email required")
148
+ end
149
+
150
+ if data[:name].nil? || data[:name].empty?
151
+ result_with_errors = result_with_errors.with_error(:validation, "Name required")
152
+ end
153
+
154
+ if data[:age] && data[:age] < 18
155
+ result_with_errors = result_with_errors.with_error(:validation, "Must be 18+")
156
+ end
157
+
158
+ # Halt only if we found errors
159
+ if result_with_errors.errors.any?
160
+ puts " [Step 1] Found #{result_with_errors.errors[:validation].size} validation error(s)"
161
+ return result_with_errors.halt
162
+ end
163
+
164
+ puts " [Step 1] Validation passed"
165
+ result_with_errors.continue(data)
166
+ }
167
+
168
+ step ->(result) {
169
+ puts " [Step 2] Processing valid data..."
170
+ result.continue("Processed: #{result.value}")
171
+ }
172
+ end
173
+
174
+ invalid_data = { email: "", name: "John", age: 16 }
175
+ result4 = multi_validation_pipeline.call(SimpleFlow::Result.new(invalid_data))
176
+ puts "\nContinue? #{result4.continue?}"
177
+ puts "Errors found: #{result4.errors[:validation].size}"
178
+ puts "All errors: #{result4.errors[:validation]}"
179
+ puts "Note: Step 2 didn't execute because validation failed"
180
+ puts
181
+
182
+ # Example 5: Comparing sequential vs parallel execution
183
+ puts "\nExample 5: Sequential vs Parallel Behavior"
184
+ puts "-" * 60
185
+ puts
186
+
187
+ # Sequential pipeline - each step depends on previous
188
+ sequential = SimpleFlow::Pipeline.new do
189
+ step ->(result) {
190
+ puts " [Sequential 1] Step A"
191
+ result.continue(result.value)
192
+ }
193
+
194
+ step ->(result) {
195
+ puts " [Sequential 2] Step B (depends on A)"
196
+ result.continue(result.value)
197
+ }
198
+
199
+ step ->(result) {
200
+ puts " [Sequential 3] Step C (depends on B)"
201
+ result.continue(result.value)
202
+ }
203
+ end
204
+
205
+ puts "Sequential execution order:"
206
+ sequential.call(SimpleFlow::Result.new(nil))
207
+
208
+ puts "\nParallel execution (for comparison):"
209
+
210
+ # Parallel pipeline - explicit dependencies
211
+ parallel = SimpleFlow::Pipeline.new do
212
+ step :step_a, ->(result) {
213
+ puts " [Parallel] Step A (no dependencies)"
214
+ result.continue(result.value)
215
+ }, depends_on: :none
216
+
217
+ step :step_b, ->(result) {
218
+ puts " [Parallel] Step B (depends on A)"
219
+ result.continue(result.value)
220
+ }, depends_on: [:step_a]
221
+
222
+ step :step_c, ->(result) {
223
+ puts " [Parallel] Step C (depends on A, runs parallel with B)"
224
+ result.continue(result.value)
225
+ }, depends_on: [:step_a]
226
+ end
227
+
228
+ parallel.call_parallel(SimpleFlow::Result.new(nil))
229
+
230
+ puts "\nNote: Sequential steps have implicit dependencies"
231
+ puts " Parallel steps require explicit depends_on declarations"
232
+
233
+ puts "\n" + "=" * 60
234
+ puts "Sequential dependencies examples completed!"
235
+ puts "=" * 60
236
+ puts
237
+ puts "Key Takeaways:"
238
+ puts " • Unnamed steps execute sequentially in definition order"
239
+ puts " • Each step implicitly depends on the previous step's success"
240
+ puts " • Pipeline halts immediately when any step returns result.halt"
241
+ puts " • Subsequent steps after a halt are never executed"
242
+ puts " • Use named steps with depends_on for parallel execution"
243
+ puts
@@ -0,0 +1,161 @@
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
+ # Using Reserved Dependency Symbols for Better Readability
9
+ #
10
+ # This example demonstrates the use of :none and :nothing reserved symbols
11
+ # for defining steps with no dependencies, providing better readability
12
+ # compared to using an empty array.
13
+
14
+ puts "=" * 60
15
+ puts "Reserved Dependency Symbols Usage"
16
+ puts "=" * 60
17
+ puts
18
+
19
+ # Example 1: Using :none symbol for clarity
20
+ puts "Example 1: Using :none Symbol for Better Readability"
21
+ puts "-" * 60
22
+ puts
23
+
24
+ pipeline = SimpleFlow::Pipeline.new do
25
+ # More readable: explicitly shows "no dependencies" using :none
26
+ step :validate, ->(result) {
27
+ puts " [Step 1] Validating input..."
28
+ result.with_context(:validated, true).continue(result.value)
29
+ }, depends_on: :none # Much cleaner than []!
30
+
31
+ # These two steps run in parallel (both depend only on :validate)
32
+ step :fetch_orders, ->(result) {
33
+ puts " [Step 2a] Fetching orders in parallel..."
34
+ sleep 0.1
35
+ result.with_context(:orders, [1, 2, 3]).continue(result.value)
36
+ }, depends_on: [:validate]
37
+
38
+ step :fetch_products, ->(result) {
39
+ puts " [Step 2b] Fetching products in parallel..."
40
+ sleep 0.1
41
+ result.with_context(:products, [:a, :b, :c]).continue(result.value)
42
+ }, depends_on: [:validate]
43
+
44
+ # This step waits for both parallel steps to complete
45
+ step :merge_data, ->(result) {
46
+ puts " [Step 3] Merging results..."
47
+ orders = result.context[:orders]
48
+ products = result.context[:products]
49
+ result.continue({
50
+ orders: orders,
51
+ products: products,
52
+ total: orders.size + products.size
53
+ })
54
+ }, depends_on: [:fetch_orders, :fetch_products]
55
+ end
56
+
57
+ result = pipeline.call_parallel(SimpleFlow::Result.new({ user_id: 123 }))
58
+ puts "\nResult: #{result.value}"
59
+ puts "Context: validated=#{result.context[:validated]}"
60
+ puts
61
+
62
+ # Example 2: Comparison with empty array syntax
63
+ puts "\nExample 2: :none Symbol vs Empty Array"
64
+ puts "-" * 60
65
+ puts
66
+
67
+ # Using :none symbol (recommended for readability)
68
+ pipeline_with_none = SimpleFlow::Pipeline.new do
69
+ step :root, ->(result) {
70
+ puts " [:none syntax] Root step with no dependencies"
71
+ result.continue(result.value + 1)
72
+ }, depends_on: :none # Clean and readable!
73
+ end
74
+
75
+ # Using empty array (functionally equivalent)
76
+ pipeline_with_array = SimpleFlow::Pipeline.new do
77
+ step :root, ->(result) {
78
+ puts " [Array syntax] Root step with no dependencies"
79
+ result.continue(result.value + 1)
80
+ }, depends_on: [] # Works, but less semantic
81
+ end
82
+
83
+ result1 = pipeline_with_none.call_parallel(SimpleFlow::Result.new(5))
84
+ result2 = pipeline_with_array.call_parallel(SimpleFlow::Result.new(5))
85
+
86
+ puts "\nBoth produce the same result: #{result1.value} == #{result2.value}"
87
+ puts "The :none symbol is simply more readable!"
88
+ puts
89
+
90
+ # Example 3: Multiple independent root steps
91
+ puts "\nExample 3: Multiple Independent Root Steps"
92
+ puts "-" * 60
93
+ puts
94
+
95
+ complex_pipeline = SimpleFlow::Pipeline.new do
96
+ # Three independent root steps - all use :none for clarity
97
+ step :load_config, ->(result) {
98
+ puts " [Root A] Loading configuration..."
99
+ result.with_context(:config, { timeout: 30 }).continue(result.value)
100
+ }, depends_on: :none
101
+
102
+ step :connect_database, ->(result) {
103
+ puts " [Root B] Connecting to database..."
104
+ result.with_context(:db, :connected).continue(result.value)
105
+ }, depends_on: :none
106
+
107
+ step :authenticate_api, ->(result) {
108
+ puts " [Root C] Authenticating API..."
109
+ result.with_context(:api_token, "abc123").continue(result.value)
110
+ }, depends_on: :none
111
+
112
+ # This step depends on all three independent root steps
113
+ step :initialize_app, ->(result) {
114
+ puts " [Final] Initializing application with all resources..."
115
+ result.continue("Application ready")
116
+ }, depends_on: [:load_config, :connect_database, :authenticate_api]
117
+ end
118
+
119
+ result3 = complex_pipeline.call_parallel(SimpleFlow::Result.new(nil))
120
+ puts "\nResult: #{result3.value}"
121
+ puts "All independent steps ran in parallel!"
122
+ puts
123
+
124
+ # Example 4: About the :none and :nothing symbols
125
+ puts "\nExample 4: Reserved Dependency Symbols"
126
+ puts "-" * 60
127
+ puts
128
+
129
+ puts "You can use :none or :nothing to indicate no dependencies:"
130
+ puts
131
+
132
+ pipeline_examples = SimpleFlow::Pipeline.new do
133
+ step :step_a, ->(result) { result.continue(1) }, depends_on: :none
134
+ step :step_b, ->(result) { result.continue(2) }, depends_on: :nothing
135
+ step :step_c, ->(result) { result.continue(3) }, depends_on: [:step_a, :none, :step_b]
136
+ end
137
+
138
+ graph = pipeline_examples.dependency_graph
139
+ puts " • :step_a dependencies: #{graph.dependencies[:step_a].inspect}"
140
+ puts " • :step_b dependencies: #{graph.dependencies[:step_b].inspect}"
141
+ puts " • :step_c dependencies: #{graph.dependencies[:step_c].inspect} (filtered!)"
142
+ puts
143
+
144
+ puts "Reserved symbols :none and :nothing:"
145
+ puts " • Automatically filtered from dependency arrays"
146
+ puts " • Functionally equivalent to []"
147
+ puts " • More semantically clear than empty array"
148
+ puts " • Cannot be used as step names (reserved)"
149
+ puts " • A signal to readers: 'this step has no dependencies'"
150
+ puts
151
+
152
+ puts "=" * 60
153
+ puts "Reserved dependency symbols examples completed!"
154
+ puts "=" * 60
155
+ puts
156
+ puts "Key Takeaways:"
157
+ puts " • Use depends_on: :none for better readability"
158
+ puts " • Equivalent to [] but more semantic"
159
+ puts " • Can mix in arrays: [:step_a, :none] becomes [:step_a]"
160
+ puts " • Makes dependency graphs easier to understand"
161
+ puts