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