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,635 @@
|
|
|
1
|
+
# Error Handling Guide
|
|
2
|
+
|
|
3
|
+
SimpleFlow provides flexible mechanisms for handling errors, validating data, and controlling pipeline flow. This guide covers comprehensive error handling strategies.
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
### The Result Object
|
|
8
|
+
|
|
9
|
+
Every step receives and returns a `Result` object with three key components:
|
|
10
|
+
|
|
11
|
+
- **value**: The data being processed
|
|
12
|
+
- **context**: Metadata and contextual information
|
|
13
|
+
- **errors**: Accumulated error messages organized by category
|
|
14
|
+
|
|
15
|
+
### Flow Control
|
|
16
|
+
|
|
17
|
+
Steps control execution flow using two methods:
|
|
18
|
+
|
|
19
|
+
- `continue(new_value)`: Proceed to next step with updated value
|
|
20
|
+
- `halt(new_value = nil)`: Stop pipeline execution
|
|
21
|
+
|
|
22
|
+
## Basic Error Handling
|
|
23
|
+
|
|
24
|
+
### Halting on Validation Failure
|
|
25
|
+
|
|
26
|
+
The simplest error handling pattern is to halt immediately when validation fails:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
30
|
+
step ->(result) {
|
|
31
|
+
age = result.value
|
|
32
|
+
|
|
33
|
+
if age < 18
|
|
34
|
+
result.halt.with_error(:validation, "Must be 18 or older")
|
|
35
|
+
else
|
|
36
|
+
result.continue(age)
|
|
37
|
+
end
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
step ->(result) {
|
|
41
|
+
# This only runs if age >= 18
|
|
42
|
+
result.continue("Approved for age #{result.value}")
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
result = pipeline.call(SimpleFlow::Result.new(15))
|
|
47
|
+
result.continue? # => false
|
|
48
|
+
result.errors # => {:validation => ["Must be 18 or older"]}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Checking Continue Status
|
|
52
|
+
|
|
53
|
+
Always check `continue?` to determine if pipeline completed successfully:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
result = pipeline.call(initial_data)
|
|
57
|
+
|
|
58
|
+
if result.continue?
|
|
59
|
+
puts "Success: #{result.value}"
|
|
60
|
+
else
|
|
61
|
+
puts "Failed with errors: #{result.errors}"
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Error Accumulation
|
|
66
|
+
|
|
67
|
+
### Collecting Multiple Errors
|
|
68
|
+
|
|
69
|
+
Instead of halting at the first error, collect all validation errors:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
73
|
+
step ->(result) {
|
|
74
|
+
password = result.value
|
|
75
|
+
|
|
76
|
+
result = if password.length < 8
|
|
77
|
+
result.with_error(:password, "Must be at least 8 characters")
|
|
78
|
+
else
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
result = unless password =~ /[A-Z]/
|
|
83
|
+
result.with_error(:password, "Must contain uppercase letters")
|
|
84
|
+
else
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
result = unless password =~ /[0-9]/
|
|
89
|
+
result.with_error(:password, "Must contain numbers")
|
|
90
|
+
else
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result.continue(password)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
step ->(result) {
|
|
98
|
+
# Check if any errors were accumulated
|
|
99
|
+
if result.errors.any?
|
|
100
|
+
result.halt.with_error(:validation, "Password requirements not met")
|
|
101
|
+
else
|
|
102
|
+
result.continue(result.value)
|
|
103
|
+
end
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
step ->(result) {
|
|
107
|
+
# Only executes if no validation errors
|
|
108
|
+
result.continue("Password accepted")
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
result = pipeline.call(SimpleFlow::Result.new("weak"))
|
|
113
|
+
result.errors
|
|
114
|
+
# => {
|
|
115
|
+
# :password => [
|
|
116
|
+
# "Must be at least 8 characters",
|
|
117
|
+
# "Must contain uppercase letters",
|
|
118
|
+
# "Must contain numbers"
|
|
119
|
+
# ],
|
|
120
|
+
# :validation => ["Password requirements not met"]
|
|
121
|
+
# }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Parallel Validation
|
|
125
|
+
|
|
126
|
+
Use parallel execution to run multiple validations concurrently:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
130
|
+
# Validate multiple fields in parallel
|
|
131
|
+
step :validate_email, ->(result) {
|
|
132
|
+
unless valid_email?(result.value[:email])
|
|
133
|
+
result.with_error(:email, "Invalid email format")
|
|
134
|
+
end
|
|
135
|
+
result.continue(result.value)
|
|
136
|
+
}, depends_on: []
|
|
137
|
+
|
|
138
|
+
step :validate_phone, ->(result) {
|
|
139
|
+
unless valid_phone?(result.value[:phone])
|
|
140
|
+
result.with_error(:phone, "Invalid phone format")
|
|
141
|
+
end
|
|
142
|
+
result.continue(result.value)
|
|
143
|
+
}, depends_on: []
|
|
144
|
+
|
|
145
|
+
step :validate_age, ->(result) {
|
|
146
|
+
if result.value[:age] < 18
|
|
147
|
+
result.with_error(:age, "Must be 18 or older")
|
|
148
|
+
end
|
|
149
|
+
result.continue(result.value)
|
|
150
|
+
}, depends_on: []
|
|
151
|
+
|
|
152
|
+
# Check all validation results
|
|
153
|
+
step :verify_validations, ->(result) {
|
|
154
|
+
if result.errors.any?
|
|
155
|
+
result.halt(result.value)
|
|
156
|
+
else
|
|
157
|
+
result.continue(result.value)
|
|
158
|
+
end
|
|
159
|
+
}, depends_on: [:validate_email, :validate_phone, :validate_age]
|
|
160
|
+
|
|
161
|
+
# Only runs if all validations pass
|
|
162
|
+
step :create_account, ->(result) {
|
|
163
|
+
result.continue("Account created successfully")
|
|
164
|
+
}, depends_on: [:verify_validations]
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Error Categories
|
|
169
|
+
|
|
170
|
+
### Organizing Errors by Type
|
|
171
|
+
|
|
172
|
+
Use symbols to categorize errors for better organization:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
step :process_order, ->(result) {
|
|
176
|
+
order = result.value
|
|
177
|
+
|
|
178
|
+
# Business logic errors
|
|
179
|
+
if order[:total] > 10000
|
|
180
|
+
return result.halt.with_error(:business_rule, "Order exceeds maximum amount")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Inventory errors
|
|
184
|
+
unless inventory_available?(order[:items])
|
|
185
|
+
return result.halt.with_error(:inventory, "Items out of stock")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Payment errors
|
|
189
|
+
unless valid_payment?(order[:payment])
|
|
190
|
+
return result.halt.with_error(:payment, "Payment method declined")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
result.continue(order)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Access errors by category
|
|
197
|
+
result = pipeline.call(order_data)
|
|
198
|
+
result.errors[:business_rule] # => ["Order exceeds maximum amount"]
|
|
199
|
+
result.errors[:inventory] # => nil
|
|
200
|
+
result.errors[:payment] # => nil
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Multiple Errors Per Category
|
|
204
|
+
|
|
205
|
+
The `with_error` method appends to existing errors in a category:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
step :validate_fields, ->(result) {
|
|
209
|
+
data = result.value
|
|
210
|
+
result_obj = result
|
|
211
|
+
|
|
212
|
+
if data[:name].nil?
|
|
213
|
+
result_obj = result_obj.with_error(:required, "Name is required")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if data[:email].nil?
|
|
217
|
+
result_obj = result_obj.with_error(:required, "Email is required")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if data[:phone].nil?
|
|
221
|
+
result_obj = result_obj.with_error(:required, "Phone is required")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
result_obj.continue(data)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# result.errors[:required] => ["Name is required", "Email is required", "Phone is required"]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Exception Handling
|
|
231
|
+
|
|
232
|
+
### Rescuing Exceptions
|
|
233
|
+
|
|
234
|
+
Wrap external calls in exception handlers:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
step :fetch_from_api, ->(result) {
|
|
238
|
+
begin
|
|
239
|
+
response = HTTP.get("https://api.example.com/data")
|
|
240
|
+
data = JSON.parse(response.body)
|
|
241
|
+
result.with_context(:api_data, data).continue(result.value)
|
|
242
|
+
rescue HTTP::Error => e
|
|
243
|
+
result.halt.with_error(:network, "API request failed: #{e.message}")
|
|
244
|
+
rescue JSON::ParserError => e
|
|
245
|
+
result.halt.with_error(:parse, "Invalid JSON response: #{e.message}")
|
|
246
|
+
rescue StandardError => e
|
|
247
|
+
result.halt.with_error(:unknown, "Unexpected error: #{e.message}")
|
|
248
|
+
end
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Retry Logic with Middleware
|
|
253
|
+
|
|
254
|
+
Implement retry logic using custom middleware:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
class RetryMiddleware
|
|
258
|
+
def initialize(callable, max_retries: 3, retry_on: [StandardError])
|
|
259
|
+
@callable = callable
|
|
260
|
+
@max_retries = max_retries
|
|
261
|
+
@retry_on = Array(retry_on)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def call(result)
|
|
265
|
+
attempts = 0
|
|
266
|
+
|
|
267
|
+
begin
|
|
268
|
+
attempts += 1
|
|
269
|
+
@callable.call(result)
|
|
270
|
+
rescue *@retry_on => e
|
|
271
|
+
if attempts < @max_retries
|
|
272
|
+
sleep(attempts ** 2) # Exponential backoff
|
|
273
|
+
retry
|
|
274
|
+
else
|
|
275
|
+
result.halt.with_error(
|
|
276
|
+
:retry_exhausted,
|
|
277
|
+
"Failed after #{@max_retries} attempts: #{e.message}"
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
285
|
+
use_middleware RetryMiddleware, max_retries: 3, retry_on: [Net::HTTPError, Timeout::Error]
|
|
286
|
+
|
|
287
|
+
step ->(result) {
|
|
288
|
+
# This step will be retried up to 3 times on network errors
|
|
289
|
+
data = fetch_from_unreliable_api(result.value)
|
|
290
|
+
result.continue(data)
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Conditional Processing
|
|
296
|
+
|
|
297
|
+
### Early Exit on Errors
|
|
298
|
+
|
|
299
|
+
Check for errors and exit early if found:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
303
|
+
step :load_data, ->(result) {
|
|
304
|
+
begin
|
|
305
|
+
data = load_file(result.value)
|
|
306
|
+
result.continue(data)
|
|
307
|
+
rescue Errno::ENOENT
|
|
308
|
+
result.halt.with_error(:file, "File not found")
|
|
309
|
+
end
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
step :validate_data, ->(result) {
|
|
313
|
+
# Only runs if load_data succeeded
|
|
314
|
+
if invalid?(result.value)
|
|
315
|
+
result.halt.with_error(:validation, "Invalid data format")
|
|
316
|
+
else
|
|
317
|
+
result.continue(result.value)
|
|
318
|
+
end
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
step :process_data, ->(result) {
|
|
322
|
+
# Only runs if both previous steps succeeded
|
|
323
|
+
processed = transform(result.value)
|
|
324
|
+
result.continue(processed)
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Conditional Flow Based on Context
|
|
330
|
+
|
|
331
|
+
Use context to make decisions about flow:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
335
|
+
step :check_user_role, ->(result) {
|
|
336
|
+
user = result.value
|
|
337
|
+
result.with_context(:role, user[:role]).continue(user)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
step :authorize_action, ->(result) {
|
|
341
|
+
case result.context[:role]
|
|
342
|
+
when :admin
|
|
343
|
+
result.with_context(:authorized, true).continue(result.value)
|
|
344
|
+
when :user
|
|
345
|
+
if can_access?(result.value)
|
|
346
|
+
result.with_context(:authorized, true).continue(result.value)
|
|
347
|
+
else
|
|
348
|
+
result.halt.with_error(:auth, "Insufficient permissions")
|
|
349
|
+
end
|
|
350
|
+
else
|
|
351
|
+
result.halt.with_error(:auth, "Unknown role")
|
|
352
|
+
end
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
step :perform_action, ->(result) {
|
|
356
|
+
# Only executes if authorized
|
|
357
|
+
result.continue("Action completed")
|
|
358
|
+
}
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Error Recovery
|
|
363
|
+
|
|
364
|
+
### Fallback Values
|
|
365
|
+
|
|
366
|
+
Provide fallback values when operations fail:
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
step :fetch_with_fallback, ->(result) {
|
|
370
|
+
begin
|
|
371
|
+
data = fetch_from_primary_api(result.value)
|
|
372
|
+
result.with_context(:source, :primary).continue(data)
|
|
373
|
+
rescue API::Error
|
|
374
|
+
# Try secondary source
|
|
375
|
+
begin
|
|
376
|
+
data = fetch_from_secondary_api(result.value)
|
|
377
|
+
result.with_context(:source, :secondary).continue(data)
|
|
378
|
+
rescue API::Error
|
|
379
|
+
# Use cached data as last resort
|
|
380
|
+
cached_data = fetch_from_cache(result.value)
|
|
381
|
+
if cached_data
|
|
382
|
+
result.with_context(:source, :cache).continue(cached_data)
|
|
383
|
+
else
|
|
384
|
+
result.halt.with_error(:data, "All data sources unavailable")
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Partial Success Handling
|
|
392
|
+
|
|
393
|
+
Continue processing even if some operations fail:
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
397
|
+
step :batch_process, ->(result) {
|
|
398
|
+
items = result.value
|
|
399
|
+
successful = []
|
|
400
|
+
failed = []
|
|
401
|
+
|
|
402
|
+
items.each do |item|
|
|
403
|
+
begin
|
|
404
|
+
processed = process_item(item)
|
|
405
|
+
successful << processed
|
|
406
|
+
rescue ProcessingError => e
|
|
407
|
+
failed << { item: item, error: e.message }
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
result_obj = result.continue(successful)
|
|
412
|
+
|
|
413
|
+
if failed.any?
|
|
414
|
+
result_obj = result_obj.with_context(:failed_items, failed)
|
|
415
|
+
result_obj = result_obj.with_error(:partial_failure, "#{failed.size} items failed to process")
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
result_obj
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
step :handle_results, ->(result) {
|
|
422
|
+
if result.context[:failed_items]
|
|
423
|
+
# Log failures but continue
|
|
424
|
+
log_failures(result.context[:failed_items])
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
result.continue("Processed #{result.value.size} items")
|
|
428
|
+
}
|
|
429
|
+
end
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## Debugging Halted Pipelines
|
|
433
|
+
|
|
434
|
+
### Using StepTracker
|
|
435
|
+
|
|
436
|
+
SimpleFlow's `StepTracker` adds context about where execution halted:
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
require 'simple_flow/step_tracker'
|
|
440
|
+
|
|
441
|
+
step_a = SimpleFlow::StepTracker.new(->(result) {
|
|
442
|
+
result.continue("Step A done")
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
step_b = SimpleFlow::StepTracker.new(->(result) {
|
|
446
|
+
result.halt.with_error(:failure, "Step B failed")
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
step_c = SimpleFlow::StepTracker.new(->(result) {
|
|
450
|
+
result.continue("Step C done")
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
454
|
+
step step_a
|
|
455
|
+
step step_b
|
|
456
|
+
step step_c
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
result = pipeline.call(SimpleFlow::Result.new(nil))
|
|
460
|
+
result.context[:halted_step] # => The step_b lambda
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Adding Debug Context
|
|
464
|
+
|
|
465
|
+
Add helpful debugging information to context:
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
step :debug_step, ->(result) {
|
|
469
|
+
result
|
|
470
|
+
.with_context(:step_name, "debug_step")
|
|
471
|
+
.with_context(:timestamp, Time.now)
|
|
472
|
+
.with_context(:input_size, result.value.size)
|
|
473
|
+
.continue(result.value)
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Validation Patterns
|
|
478
|
+
|
|
479
|
+
### Schema Validation
|
|
480
|
+
|
|
481
|
+
Validate data structure before processing:
|
|
482
|
+
|
|
483
|
+
```ruby
|
|
484
|
+
step :validate_schema, ->(result) {
|
|
485
|
+
data = result.value
|
|
486
|
+
required_fields = [:name, :email, :age]
|
|
487
|
+
|
|
488
|
+
missing = required_fields.reject { |field| data.key?(field) }
|
|
489
|
+
|
|
490
|
+
if missing.any?
|
|
491
|
+
result.halt.with_error(
|
|
492
|
+
:schema,
|
|
493
|
+
"Missing required fields: #{missing.join(', ')}"
|
|
494
|
+
)
|
|
495
|
+
else
|
|
496
|
+
result.continue(data)
|
|
497
|
+
end
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Type Validation
|
|
502
|
+
|
|
503
|
+
Check data types:
|
|
504
|
+
|
|
505
|
+
```ruby
|
|
506
|
+
step :validate_types, ->(result) {
|
|
507
|
+
data = result.value
|
|
508
|
+
errors = []
|
|
509
|
+
|
|
510
|
+
unless data[:age].is_a?(Integer)
|
|
511
|
+
errors << "age must be an integer"
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
unless data[:email].is_a?(String)
|
|
515
|
+
errors << "email must be a string"
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
if errors.any?
|
|
519
|
+
result.halt.with_error(:type, errors.join(", "))
|
|
520
|
+
else
|
|
521
|
+
result.continue(data)
|
|
522
|
+
end
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Range Validation
|
|
527
|
+
|
|
528
|
+
Validate numeric ranges:
|
|
529
|
+
|
|
530
|
+
```ruby
|
|
531
|
+
step :validate_ranges, ->(result) {
|
|
532
|
+
data = result.value
|
|
533
|
+
|
|
534
|
+
if data[:age] < 0 || data[:age] > 120
|
|
535
|
+
return result.halt.with_error(:range, "Age must be between 0 and 120")
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
if data[:quantity] < 1
|
|
539
|
+
return result.halt.with_error(:range, "Quantity must be at least 1")
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
result.continue(data)
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
## Real-World Example
|
|
547
|
+
|
|
548
|
+
Complete error handling in an order processing pipeline:
|
|
549
|
+
|
|
550
|
+
```ruby
|
|
551
|
+
order_pipeline = SimpleFlow::Pipeline.new do
|
|
552
|
+
# Validate order structure
|
|
553
|
+
step :validate_order, ->(result) {
|
|
554
|
+
order = result.value
|
|
555
|
+
errors = []
|
|
556
|
+
|
|
557
|
+
errors << "Missing customer email" unless order[:customer][:email]
|
|
558
|
+
errors << "No items in order" if order[:items].empty?
|
|
559
|
+
errors << "Missing payment method" unless order[:payment][:card_token]
|
|
560
|
+
|
|
561
|
+
if errors.any?
|
|
562
|
+
result.halt.with_error(:validation, errors.join(", "))
|
|
563
|
+
else
|
|
564
|
+
result.with_context(:validated_at, Time.now).continue(order)
|
|
565
|
+
end
|
|
566
|
+
}, depends_on: []
|
|
567
|
+
|
|
568
|
+
# Check inventory with error handling
|
|
569
|
+
step :check_inventory, ->(result) {
|
|
570
|
+
begin
|
|
571
|
+
inventory_results = InventoryService.check_availability(result.value[:items])
|
|
572
|
+
|
|
573
|
+
if inventory_results.all? { |r| r[:available] }
|
|
574
|
+
result.with_context(:inventory_check, inventory_results).continue(result.value)
|
|
575
|
+
else
|
|
576
|
+
unavailable = inventory_results.reject { |r| r[:available] }
|
|
577
|
+
result.halt.with_error(
|
|
578
|
+
:inventory,
|
|
579
|
+
"Items unavailable: #{unavailable.map { |i| i[:product_id] }.join(', ')}"
|
|
580
|
+
)
|
|
581
|
+
end
|
|
582
|
+
rescue InventoryService::Error => e
|
|
583
|
+
result.halt.with_error(:service, "Inventory service error: #{e.message}")
|
|
584
|
+
end
|
|
585
|
+
}, depends_on: [:validate_order]
|
|
586
|
+
|
|
587
|
+
# Process payment with retries
|
|
588
|
+
step :process_payment, ->(result) {
|
|
589
|
+
total = result.context[:total]
|
|
590
|
+
|
|
591
|
+
payment_result = PaymentService.process_payment(
|
|
592
|
+
total,
|
|
593
|
+
result.value[:payment][:card_token]
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
case payment_result[:status]
|
|
597
|
+
when :success
|
|
598
|
+
result.with_context(:payment, payment_result).continue(result.value)
|
|
599
|
+
when :declined
|
|
600
|
+
result.halt.with_error(:payment, "Card declined: #{payment_result[:reason]}")
|
|
601
|
+
when :insufficient_funds
|
|
602
|
+
result.halt.with_error(:payment, "Insufficient funds")
|
|
603
|
+
else
|
|
604
|
+
result.halt.with_error(:payment, "Payment processing failed")
|
|
605
|
+
end
|
|
606
|
+
}, depends_on: [:check_inventory]
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Execute and handle errors
|
|
610
|
+
result = order_pipeline.call_parallel(order_data)
|
|
611
|
+
|
|
612
|
+
if result.continue?
|
|
613
|
+
puts "Order processed successfully"
|
|
614
|
+
send_confirmation_email(result.value)
|
|
615
|
+
else
|
|
616
|
+
puts "Order failed:"
|
|
617
|
+
result.errors.each do |category, messages|
|
|
618
|
+
puts " #{category}: #{messages.join(', ')}"
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# Handle specific error types
|
|
622
|
+
if result.errors[:payment]
|
|
623
|
+
log_payment_failure(result.value)
|
|
624
|
+
elsif result.errors[:inventory]
|
|
625
|
+
notify_inventory_team(result.value)
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
## Related Documentation
|
|
631
|
+
|
|
632
|
+
- [Validation Patterns](validation-patterns.md) - Common validation strategies
|
|
633
|
+
- [Complex Workflows](complex-workflows.md) - Building sophisticated pipelines
|
|
634
|
+
- [Result API](../api/result.md) - Complete Result class reference
|
|
635
|
+
- [Pipeline API](../api/pipeline.md) - Pipeline class reference
|