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,389 @@
1
+ # Middleware
2
+
3
+ Middleware provides a way to add cross-cutting concerns to your pipeline without modifying individual steps.
4
+
5
+ ## Overview
6
+
7
+ Middleware wraps steps using the decorator pattern, allowing you to:
8
+
9
+ - Log step execution
10
+ - Measure performance
11
+ - Add authentication/authorization
12
+ - Handle retries
13
+ - Cache results
14
+ - Track metrics
15
+
16
+ ## Built-in Middleware
17
+
18
+ ### Logging Middleware
19
+
20
+ Logs before and after each step execution:
21
+
22
+ ```ruby
23
+ require 'simple_flow'
24
+
25
+ pipeline = SimpleFlow::Pipeline.new do
26
+ use_middleware SimpleFlow::MiddleWare::Logging
27
+
28
+ step ->(result) { result.continue(process_data(result.value)) }
29
+ step ->(result) { result.continue(validate_data(result.value)) }
30
+ end
31
+ ```
32
+
33
+ Output:
34
+ ```
35
+ [SimpleFlow] Before step: #<Proc:0x00007f8b1c0b4f00>
36
+ [SimpleFlow] After step: #<Proc:0x00007f8b1c0b4f00>
37
+ [SimpleFlow] Before step: #<Proc:0x00007f8b1c0b5200>
38
+ [SimpleFlow] After step: #<Proc:0x00007f8b1c0b5200>
39
+ ```
40
+
41
+ ### Instrumentation Middleware
42
+
43
+ Measures execution time and tracks API usage:
44
+
45
+ ```ruby
46
+ pipeline = SimpleFlow::Pipeline.new do
47
+ use_middleware SimpleFlow::MiddleWare::Instrumentation, api_key: 'my-app-key'
48
+
49
+ step ->(result) { result.continue(fetch_data(result.value)) }
50
+ step ->(result) { result.continue(process_data(result.value)) }
51
+ end
52
+ ```
53
+
54
+ Output:
55
+ ```
56
+ Instrumentation: my-app-key took 0.0423s
57
+ Instrumentation: my-app-key took 0.0156s
58
+ ```
59
+
60
+ ## Creating Custom Middleware
61
+
62
+ Middleware is any class that:
63
+
64
+ 1. Accepts a `callable` and optional `options` in its initializer
65
+ 2. Implements a `call(result)` method
66
+ 3. Calls `@callable.call(result)` to execute the wrapped step
67
+
68
+ ### Basic Template
69
+
70
+ ```ruby
71
+ class MyMiddleware
72
+ def initialize(callable, **options)
73
+ @callable = callable
74
+ @options = options
75
+ end
76
+
77
+ def call(result)
78
+ # Before logic
79
+ puts "Before step with options: #{@options.inspect}"
80
+
81
+ # Execute the step
82
+ result = @callable.call(result)
83
+
84
+ # After logic
85
+ puts "After step, value: #{result.value.inspect}"
86
+
87
+ result
88
+ end
89
+ end
90
+ ```
91
+
92
+ ### Example: Retry Middleware
93
+
94
+ ```ruby
95
+ class RetryMiddleware
96
+ def initialize(callable, max_retries: 3, backoff: 1.0)
97
+ @callable = callable
98
+ @max_retries = max_retries
99
+ @backoff = backoff
100
+ end
101
+
102
+ def call(result)
103
+ attempts = 0
104
+
105
+ begin
106
+ @callable.call(result)
107
+ rescue StandardError => e
108
+ attempts += 1
109
+
110
+ if attempts < @max_retries
111
+ sleep(@backoff * attempts)
112
+ retry
113
+ else
114
+ result.with_error(:retry_exhausted, e.message).halt
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ # Usage
121
+ pipeline = SimpleFlow::Pipeline.new do
122
+ use_middleware RetryMiddleware, max_retries: 5, backoff: 2.0
123
+
124
+ step ->(result) {
125
+ # This will be retried up to 5 times
126
+ data = unreliable_api_call(result.value)
127
+ result.continue(data)
128
+ }
129
+ end
130
+ ```
131
+
132
+ ### Example: Authentication Middleware
133
+
134
+ ```ruby
135
+ class AuthenticationMiddleware
136
+ def initialize(callable, required_role: nil)
137
+ @callable = callable
138
+ @required_role = required_role
139
+ end
140
+
141
+ def call(result)
142
+ user = result.context[:current_user]
143
+
144
+ unless user
145
+ return result
146
+ .with_error(:authentication, 'User not authenticated')
147
+ .halt
148
+ end
149
+
150
+ if @required_role && !user.has_role?(@required_role)
151
+ return result
152
+ .with_error(:authorization, "Requires #{@required_role} role")
153
+ .halt
154
+ end
155
+
156
+ @callable.call(result)
157
+ end
158
+ end
159
+
160
+ # Usage
161
+ pipeline = SimpleFlow::Pipeline.new do
162
+ use_middleware AuthenticationMiddleware, required_role: :admin
163
+
164
+ step ->(result) {
165
+ # This only runs if user is authenticated and has admin role
166
+ result.continue(sensitive_operation(result.value))
167
+ }
168
+ end
169
+ ```
170
+
171
+ ### Example: Caching Middleware
172
+
173
+ ```ruby
174
+ class CachingMiddleware
175
+ def initialize(callable, cache:, ttl: 3600)
176
+ @callable = callable
177
+ @cache = cache
178
+ @ttl = ttl
179
+ end
180
+
181
+ def call(result)
182
+ cache_key = generate_cache_key(result)
183
+
184
+ # Try cache first
185
+ if cached = @cache.get(cache_key)
186
+ return result
187
+ .continue(cached)
188
+ .with_context(:cache_hit, true)
189
+ end
190
+
191
+ # Execute step
192
+ result = @callable.call(result)
193
+
194
+ # Cache the result
195
+ @cache.set(cache_key, result.value, ttl: @ttl) if result.continue?
196
+
197
+ result.with_context(:cache_hit, false)
198
+ end
199
+
200
+ private
201
+
202
+ def generate_cache_key(result)
203
+ Digest::MD5.hexdigest(result.value.to_json)
204
+ end
205
+ end
206
+
207
+ # Usage
208
+ pipeline = SimpleFlow::Pipeline.new do
209
+ use_middleware CachingMiddleware, cache: Redis.new, ttl: 1800
210
+
211
+ step ->(result) {
212
+ # Expensive operation that will be cached
213
+ data = expensive_database_query(result.value)
214
+ result.continue(data)
215
+ }
216
+ end
217
+ ```
218
+
219
+ ## Middleware Order
220
+
221
+ Middleware is applied in reverse order (last declared = innermost wrapper):
222
+
223
+ ```ruby
224
+ pipeline = SimpleFlow::Pipeline.new do
225
+ use_middleware MiddlewareA # Applied third (outermost)
226
+ use_middleware MiddlewareB # Applied second
227
+ use_middleware MiddlewareC # Applied first (innermost)
228
+
229
+ step ->(result) { result.continue('data') }
230
+ end
231
+ ```
232
+
233
+ Execution order:
234
+ ```
235
+ MiddlewareA before
236
+ MiddlewareB before
237
+ MiddlewareC before
238
+ Step executes
239
+ MiddlewareC after
240
+ MiddlewareB after
241
+ MiddlewareA after
242
+ ```
243
+
244
+ ## Combining Multiple Middleware
245
+
246
+ ```ruby
247
+ pipeline = SimpleFlow::Pipeline.new do
248
+ # Logging (outermost)
249
+ use_middleware SimpleFlow::MiddleWare::Logging
250
+
251
+ # Authentication
252
+ use_middleware AuthenticationMiddleware, required_role: :user
253
+
254
+ # Caching
255
+ use_middleware CachingMiddleware, cache: Rails.cache
256
+
257
+ # Retry logic
258
+ use_middleware RetryMiddleware, max_retries: 3
259
+
260
+ # Instrumentation (innermost)
261
+ use_middleware SimpleFlow::MiddleWare::Instrumentation, api_key: 'app'
262
+
263
+ step ->(result) { result.continue(process(result.value)) }
264
+ end
265
+ ```
266
+
267
+ ## Conditional Middleware
268
+
269
+ Apply middleware based on conditions:
270
+
271
+ ```ruby
272
+ pipeline = SimpleFlow::Pipeline.new do
273
+ use_middleware SimpleFlow::MiddleWare::Logging if ENV['DEBUG']
274
+ use_middleware CachingMiddleware, cache: cache if Rails.env.production?
275
+
276
+ step ->(result) { result.continue(process(result.value)) }
277
+ end
278
+ ```
279
+
280
+ ## Testing Middleware
281
+
282
+ ```ruby
283
+ require 'minitest/autorun'
284
+
285
+ class MyMiddlewareTest < Minitest::Test
286
+ def test_middleware_execution
287
+ step = ->(result) { result.continue('processed') }
288
+ middleware = MyMiddleware.new(step, option: 'value')
289
+
290
+ input = SimpleFlow::Result.new('input')
291
+ output = middleware.call(input)
292
+
293
+ assert_equal 'processed', output.value
294
+ end
295
+
296
+ def test_middleware_adds_context
297
+ step = ->(result) { result.continue(result.value) }
298
+ middleware = TimingMiddleware.new(step)
299
+
300
+ input = SimpleFlow::Result.new('data')
301
+ output = middleware.call(input)
302
+
303
+ assert output.context[:execution_time]
304
+ end
305
+ end
306
+ ```
307
+
308
+ ## Best Practices
309
+
310
+ 1. **Keep middleware focused**: Each middleware should handle one concern
311
+ 2. **Preserve the result**: Always call `@callable.call(result)`
312
+ 3. **Don't swallow errors**: Let exceptions propagate unless you're handling retries
313
+ 4. **Use context for metadata**: Add timing, cache hits, etc. to context
314
+ 5. **Make options explicit**: Use keyword arguments for clarity
315
+ 6. **Test in isolation**: Middleware should be independently testable
316
+ 7. **Document side effects**: Clearly document any state changes
317
+
318
+ ## Common Use Cases
319
+
320
+ ### Performance Monitoring
321
+
322
+ ```ruby
323
+ class PerformanceMiddleware
324
+ def initialize(callable, threshold: 1.0)
325
+ @callable = callable
326
+ @threshold = threshold
327
+ end
328
+
329
+ def call(result)
330
+ start_time = Time.now
331
+ result = @callable.call(result)
332
+ duration = Time.now - start_time
333
+
334
+ if duration > @threshold
335
+ warn "Slow step: #{duration}s (threshold: #{@threshold}s)"
336
+ end
337
+
338
+ result.with_context(:duration, duration)
339
+ end
340
+ end
341
+ ```
342
+
343
+ ### Error Enrichment
344
+
345
+ ```ruby
346
+ class ErrorEnrichmentMiddleware
347
+ def initialize(callable)
348
+ @callable = callable
349
+ end
350
+
351
+ def call(result)
352
+ @callable.call(result)
353
+ rescue StandardError => e
354
+ result
355
+ .with_error(:exception, e.message)
356
+ .with_context(:exception_class, e.class.name)
357
+ .with_context(:backtrace, e.backtrace.first(5))
358
+ .halt
359
+ end
360
+ end
361
+ ```
362
+
363
+ ### Request ID Tracking
364
+
365
+ ```ruby
366
+ class RequestIDMiddleware
367
+ def initialize(callable)
368
+ @callable = callable
369
+ end
370
+
371
+ def call(result)
372
+ request_id = result.context[:request_id] || SecureRandom.uuid
373
+
374
+ result_with_id = result.with_context(:request_id, request_id)
375
+
376
+ Thread.current[:request_id] = request_id
377
+ result = @callable.call(result_with_id)
378
+ Thread.current[:request_id] = nil
379
+
380
+ result
381
+ end
382
+ end
383
+ ```
384
+
385
+ ## Next Steps
386
+
387
+ - [Pipeline](pipeline.md) - Learn how middleware integrates with pipelines
388
+ - [Flow Control](flow-control.md) - Controlling execution flow
389
+ - [Error Handling Guide](../guides/error-handling.md) - Comprehensive error strategies
@@ -0,0 +1,219 @@
1
+ # Core Concepts
2
+
3
+ Understanding SimpleFlow's fundamental concepts will help you build robust pipelines.
4
+
5
+ ## Architecture
6
+
7
+ SimpleFlow is built on four core components:
8
+
9
+ ```mermaid
10
+ graph TD
11
+ A[Result] -->|passed to| B[Step]
12
+ B -->|transformed by| C[Middleware]
13
+ C -->|orchestrated by| D[Pipeline]
14
+ D -->|produces| A
15
+ ```
16
+
17
+ ### 1. Result
18
+
19
+ An **immutable value object** that carries:
20
+ - **Value**: The data being processed
21
+ - **Context**: Metadata accumulated during processing
22
+ - **Errors**: Validation or processing errors
23
+ - **Continue Flag**: Whether to continue pipeline execution
24
+
25
+ [Learn more about Results](result.md)
26
+
27
+ ### 2. Step
28
+
29
+ A **callable object** (usually a lambda) that:
30
+ - Receives a Result
31
+ - Performs some operation
32
+ - Returns a new Result
33
+
34
+ [Learn more about Steps](steps.md)
35
+
36
+ ### 3. Pipeline
37
+
38
+ An **orchestrator** that:
39
+ - Holds a sequence of steps
40
+ - Applies middleware to steps
41
+ - Executes steps in order
42
+ - Short-circuits on halt
43
+
44
+ [Learn more about Pipelines](pipeline.md)
45
+
46
+ ### 4. Middleware
47
+
48
+ A **decorator** that:
49
+ - Wraps steps with additional behavior
50
+ - Adds cross-cutting concerns (logging, timing, etc.)
51
+ - Applied in reverse order to all steps
52
+
53
+ [Learn more about Middleware](middleware.md)
54
+
55
+ ## Data Flow
56
+
57
+ Here's how data flows through a pipeline:
58
+
59
+ ```mermaid
60
+ sequenceDiagram
61
+ participant Client
62
+ participant Pipeline
63
+ participant Middleware
64
+ participant Step
65
+ participant Result
66
+
67
+ Client->>Pipeline: call(initial_result)
68
+ Pipeline->>Middleware: wrap steps
69
+ loop Each Step
70
+ Pipeline->>Step: call(result)
71
+ Step->>Result: transform
72
+ Result-->>Step: new result
73
+ Step-->>Pipeline: new result
74
+ alt continue?
75
+ Pipeline->>Pipeline: next step
76
+ else halted
77
+ Pipeline-->>Client: final result
78
+ end
79
+ end
80
+ Pipeline-->>Client: final result
81
+ ```
82
+
83
+ ## Key Principles
84
+
85
+ ### Immutability
86
+
87
+ Results are **never modified**, only **copied with changes**:
88
+
89
+ ```ruby
90
+ original = SimpleFlow::Result.new(42)
91
+ updated = original.continue(43)
92
+
93
+ original.value # => 42 (unchanged)
94
+ updated.value # => 43 (new object)
95
+ ```
96
+
97
+ This makes pipelines thread-safe and easier to reason about.
98
+
99
+ ### Composability
100
+
101
+ Steps are **simple, reusable functions**:
102
+
103
+ ```ruby
104
+ # Define reusable steps
105
+ validate_email = ->(result) { ... }
106
+ validate_age = ->(result) { ... }
107
+ validate_password = ->(result) { ... }
108
+
109
+ # Compose into pipelines
110
+ pipeline1 = SimpleFlow::Pipeline.new do
111
+ step validate_email
112
+ step validate_age
113
+ end
114
+
115
+ pipeline2 = SimpleFlow::Pipeline.new do
116
+ step validate_email
117
+ step validate_password
118
+ end
119
+ ```
120
+
121
+ ### Flow Control
122
+
123
+ Steps decide whether the pipeline should **continue or halt**:
124
+
125
+ ```ruby
126
+ step ->(result) {
127
+ if condition_met?
128
+ result.continue(new_value) # Continue to next step
129
+ else
130
+ result.halt(value).with_error(:key, "message") # Stop pipeline
131
+ end
132
+ }
133
+ ```
134
+
135
+ ### Context Accumulation
136
+
137
+ Metadata accumulates across steps:
138
+
139
+ ```ruby
140
+ pipeline = SimpleFlow::Pipeline.new do
141
+ step ->(result) { result.with_context(:step1, "data").continue(result.value) }
142
+ step ->(result) { result.with_context(:step2, "more").continue(result.value) }
143
+ end
144
+
145
+ result = pipeline.call(SimpleFlow::Result.new(42))
146
+ result.context # => {:step1=>"data", :step2=>"more"}
147
+ ```
148
+
149
+ ## Design Patterns
150
+
151
+ SimpleFlow implements several design patterns:
152
+
153
+ ### Pipeline Pattern
154
+ Sequential processing with short-circuit capability.
155
+
156
+ ### Decorator Pattern
157
+ Middleware wraps steps to add behavior without modifying them.
158
+
159
+ ### Immutable Value Object
160
+ Results are never modified, preventing side effects.
161
+
162
+ ### Builder Pattern
163
+ DSL for intuitive pipeline configuration.
164
+
165
+ ### Chain of Responsibility
166
+ Each step can handle or pass along the result.
167
+
168
+ ## Common Patterns
169
+
170
+ ### Validation Pipeline
171
+
172
+ ```ruby
173
+ pipeline = SimpleFlow::Pipeline.new do
174
+ step ->(result) { validate_required_fields(result) }
175
+ step ->(result) { validate_format(result) }
176
+ step ->(result) { validate_business_rules(result) }
177
+
178
+ step ->(result) {
179
+ # Halt if any errors accumulated
180
+ result.errors.any? ? result.halt : result.continue(result.value)
181
+ }
182
+ end
183
+ ```
184
+
185
+ ### Data Transformation Pipeline
186
+
187
+ ```ruby
188
+ pipeline = SimpleFlow::Pipeline.new do
189
+ step ->(result) { parse_input(result) }
190
+ step ->(result) { transform_data(result) }
191
+ step ->(result) { format_output(result) }
192
+ end
193
+ ```
194
+
195
+ ### Enrichment Pipeline
196
+
197
+ ```ruby
198
+ pipeline = SimpleFlow::Pipeline.new do
199
+ step ->(result) { fetch_base_data(result) }
200
+
201
+ parallel do
202
+ step ->(result) { enrich_with_user_data(result) }
203
+ step ->(result) { enrich_with_order_data(result) }
204
+ step ->(result) { enrich_with_analytics(result) }
205
+ end
206
+
207
+ step ->(result) { aggregate_enrichments(result) }
208
+ end
209
+ ```
210
+
211
+ ## Next Steps
212
+
213
+ Explore each component in detail:
214
+
215
+ - [Result API](result.md) - Immutable value objects
216
+ - [Pipeline API](pipeline.md) - Orchestrating steps
217
+ - [Steps Guide](steps.md) - Writing effective steps
218
+ - [Middleware Guide](middleware.md) - Cross-cutting concerns
219
+ - [Flow Control](flow-control.md) - Halting and continuing