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,496 @@
|
|
|
1
|
+
# Validation Patterns
|
|
2
|
+
|
|
3
|
+
This guide presents common validation patterns used with SimpleFlow for data validation, business rule enforcement, and input sanitization.
|
|
4
|
+
|
|
5
|
+
## Basic Validation Patterns
|
|
6
|
+
|
|
7
|
+
### Required Fields
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
step :validate_required, ->(result) {
|
|
11
|
+
data = result.value
|
|
12
|
+
required = [:name, :email, :password]
|
|
13
|
+
missing = required.reject { |field| data[field] && !data[field].empty? }
|
|
14
|
+
|
|
15
|
+
if missing.any?
|
|
16
|
+
result.halt.with_error(:required, "Missing fields: #{missing.join(', ')}")
|
|
17
|
+
else
|
|
18
|
+
result.continue(data)
|
|
19
|
+
end
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Format Validation
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
step :validate_formats, ->(result) {
|
|
27
|
+
data = result.value
|
|
28
|
+
errors = []
|
|
29
|
+
|
|
30
|
+
unless data[:email] =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
|
31
|
+
errors << "Invalid email format"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
unless data[:phone] =~ /\A\+?\d{10,15}\z/
|
|
35
|
+
errors << "Invalid phone format"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if errors.any?
|
|
39
|
+
result.halt.with_error(:format, errors.join(", "))
|
|
40
|
+
else
|
|
41
|
+
result.continue(data)
|
|
42
|
+
end
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Range Validation
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
step :validate_ranges, ->(result) {
|
|
50
|
+
data = result.value
|
|
51
|
+
|
|
52
|
+
if data[:age] && (data[:age] < 0 || data[:age] > 120)
|
|
53
|
+
return result.halt.with_error(:range, "Age must be between 0 and 120")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if data[:quantity] && data[:quantity] < 1
|
|
57
|
+
return result.halt.with_error(:range, "Quantity must be at least 1")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result.continue(data)
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Type Validation
|
|
65
|
+
|
|
66
|
+
### Type Checking
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
step :validate_types, ->(result) {
|
|
70
|
+
data = result.value
|
|
71
|
+
type_specs = {
|
|
72
|
+
name: String,
|
|
73
|
+
age: Integer,
|
|
74
|
+
active: [TrueClass, FalseClass],
|
|
75
|
+
tags: Array
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
errors = type_specs.map do |field, expected_type|
|
|
79
|
+
value = data[field]
|
|
80
|
+
next if value.nil?
|
|
81
|
+
|
|
82
|
+
expected = Array(expected_type)
|
|
83
|
+
unless expected.any? { |type| value.is_a?(type) }
|
|
84
|
+
"#{field} must be #{expected.join(' or ')}, got #{value.class}"
|
|
85
|
+
end
|
|
86
|
+
end.compact
|
|
87
|
+
|
|
88
|
+
if errors.any?
|
|
89
|
+
result.halt.with_error(:type, errors.join(", "))
|
|
90
|
+
else
|
|
91
|
+
result.continue(data)
|
|
92
|
+
end
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Parallel Validation
|
|
97
|
+
|
|
98
|
+
### Independent Field Validation
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
102
|
+
step :validate_email, ->(result) {
|
|
103
|
+
unless valid_email?(result.value[:email])
|
|
104
|
+
result.with_error(:email, "Invalid email")
|
|
105
|
+
end
|
|
106
|
+
result.continue(result.value)
|
|
107
|
+
}, depends_on: []
|
|
108
|
+
|
|
109
|
+
step :validate_password, ->(result) {
|
|
110
|
+
password = result.value[:password]
|
|
111
|
+
errors = []
|
|
112
|
+
errors << "Too short" if password.length < 8
|
|
113
|
+
errors << "Need uppercase" unless password =~ /[A-Z]/
|
|
114
|
+
errors << "Need number" unless password =~ /[0-9]/
|
|
115
|
+
|
|
116
|
+
errors.each { |err| result = result.with_error(:password, err) }
|
|
117
|
+
result.continue(result.value)
|
|
118
|
+
}, depends_on: []
|
|
119
|
+
|
|
120
|
+
step :validate_phone, ->(result) {
|
|
121
|
+
unless valid_phone?(result.value[:phone])
|
|
122
|
+
result.with_error(:phone, "Invalid phone")
|
|
123
|
+
end
|
|
124
|
+
result.continue(result.value)
|
|
125
|
+
}, depends_on: []
|
|
126
|
+
|
|
127
|
+
step :check_errors, ->(result) {
|
|
128
|
+
if result.errors.any?
|
|
129
|
+
result.halt(result.value)
|
|
130
|
+
else
|
|
131
|
+
result.continue(result.value)
|
|
132
|
+
end
|
|
133
|
+
}, depends_on: [:validate_email, :validate_password, :validate_phone]
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Business Rule Validation
|
|
138
|
+
|
|
139
|
+
### Single Rule Validation
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
step :validate_business_rules, ->(result) {
|
|
143
|
+
order = result.value
|
|
144
|
+
|
|
145
|
+
# Maximum order amount
|
|
146
|
+
if order[:total] > 10000
|
|
147
|
+
return result.halt.with_error(:business, "Order exceeds maximum amount")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Minimum order for free shipping
|
|
151
|
+
if order[:total] < 50 && order[:shipping_method] == :free
|
|
152
|
+
return result.halt.with_error(:business, "Free shipping requires $50 minimum")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Age restriction
|
|
156
|
+
if order[:items].any? { |i| i[:age_restricted] } && order[:customer][:age] < 21
|
|
157
|
+
return result.halt.with_error(:business, "Age-restricted items require customer to be 21+")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
result.continue(order)
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Conditional Business Rules
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
step :apply_discount_rules, ->(result) {
|
|
168
|
+
order = result.value
|
|
169
|
+
customer = result.context[:customer]
|
|
170
|
+
|
|
171
|
+
discount = 0
|
|
172
|
+
|
|
173
|
+
# VIP customers get 20% off
|
|
174
|
+
if customer[:vip]
|
|
175
|
+
discount = [discount, 0.20].max
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Orders over $100 get 10% off
|
|
179
|
+
if order[:subtotal] > 100
|
|
180
|
+
discount = [discount, 0.10].max
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# First-time customers get 15% off
|
|
184
|
+
if customer[:order_count] == 0
|
|
185
|
+
discount = [discount, 0.15].max
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
result
|
|
189
|
+
.with_context(:discount_rate, discount)
|
|
190
|
+
.with_context(:discount_amount, order[:subtotal] * discount)
|
|
191
|
+
.continue(order)
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Custom Validators
|
|
196
|
+
|
|
197
|
+
### Reusable Validator Classes
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
class EmailValidator
|
|
201
|
+
def self.call(result)
|
|
202
|
+
email = result.value[:email]
|
|
203
|
+
|
|
204
|
+
errors = []
|
|
205
|
+
errors << "Email is required" if email.nil? || email.empty?
|
|
206
|
+
errors << "Invalid email format" unless email =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
|
207
|
+
|
|
208
|
+
if errors.any?
|
|
209
|
+
errors.each { |error| result = result.with_error(:email, error) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
result.continue(result.value)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
class PasswordValidator
|
|
217
|
+
MIN_LENGTH = 8
|
|
218
|
+
|
|
219
|
+
def self.call(result)
|
|
220
|
+
password = result.value[:password]
|
|
221
|
+
|
|
222
|
+
errors = []
|
|
223
|
+
errors << "Password is required" if password.nil? || password.empty?
|
|
224
|
+
errors << "Password too short" if password && password.length < MIN_LENGTH
|
|
225
|
+
errors << "Must contain uppercase" unless password =~ /[A-Z]/
|
|
226
|
+
errors << "Must contain lowercase" unless password =~ /[a-z]/
|
|
227
|
+
errors << "Must contain number" unless password =~ /[0-9]/
|
|
228
|
+
|
|
229
|
+
if errors.any?
|
|
230
|
+
errors.each { |error| result = result.with_error(:password, error) }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
result.continue(result.value)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
238
|
+
step :validate_email, EmailValidator, depends_on: []
|
|
239
|
+
step :validate_password, PasswordValidator, depends_on: []
|
|
240
|
+
|
|
241
|
+
step :check_validations, ->(result) {
|
|
242
|
+
if result.errors.any?
|
|
243
|
+
result.halt(result.value)
|
|
244
|
+
else
|
|
245
|
+
result.continue(result.value)
|
|
246
|
+
end
|
|
247
|
+
}, depends_on: [:validate_email, :validate_password]
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Cross-Field Validation
|
|
252
|
+
|
|
253
|
+
### Dependent Fields
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
step :validate_shipping, ->(result) {
|
|
257
|
+
order = result.value
|
|
258
|
+
|
|
259
|
+
# If express shipping is selected, shipping address is required
|
|
260
|
+
if order[:shipping_method] == :express
|
|
261
|
+
unless order[:shipping_address]
|
|
262
|
+
return result.halt.with_error(:shipping, "Express shipping requires address")
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# If international shipping, country is required
|
|
267
|
+
if order[:international]
|
|
268
|
+
unless order[:shipping_address][:country]
|
|
269
|
+
return result.halt.with_error(:shipping, "International shipping requires country")
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
result.continue(order)
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Mutually Exclusive Fields
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
step :validate_payment, ->(result) {
|
|
281
|
+
payment = result.value[:payment]
|
|
282
|
+
|
|
283
|
+
methods_present = [
|
|
284
|
+
payment[:credit_card],
|
|
285
|
+
payment[:paypal],
|
|
286
|
+
payment[:bank_transfer]
|
|
287
|
+
].count { |m| m }
|
|
288
|
+
|
|
289
|
+
if methods_present == 0
|
|
290
|
+
result.halt.with_error(:payment, "Payment method required")
|
|
291
|
+
elsif methods_present > 1
|
|
292
|
+
result.halt.with_error(:payment, "Only one payment method allowed")
|
|
293
|
+
else
|
|
294
|
+
result.continue(result.value)
|
|
295
|
+
end
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## External Validation
|
|
300
|
+
|
|
301
|
+
### API-Based Validation
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
step :validate_address, ->(result) {
|
|
305
|
+
begin
|
|
306
|
+
address = result.value[:shipping_address]
|
|
307
|
+
validation = AddressValidator.validate(address)
|
|
308
|
+
|
|
309
|
+
if validation[:valid]
|
|
310
|
+
result
|
|
311
|
+
.with_context(:validated_address, validation[:normalized])
|
|
312
|
+
.continue(result.value)
|
|
313
|
+
else
|
|
314
|
+
result.halt.with_error(:address, validation[:errors].join(", "))
|
|
315
|
+
end
|
|
316
|
+
rescue AddressValidator::Error => e
|
|
317
|
+
result.halt.with_error(:validation_service, "Address validation failed: #{e.message}")
|
|
318
|
+
end
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Database Validation
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
step :validate_unique_email, ->(result) {
|
|
326
|
+
email = result.value[:email]
|
|
327
|
+
|
|
328
|
+
if User.exists?(email: email)
|
|
329
|
+
result.halt.with_error(:uniqueness, "Email already registered")
|
|
330
|
+
else
|
|
331
|
+
result.continue(result.value)
|
|
332
|
+
end
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
step :validate_referral_code, ->(result) {
|
|
336
|
+
code = result.value[:referral_code]
|
|
337
|
+
return result.continue(result.value) if code.nil?
|
|
338
|
+
|
|
339
|
+
referrer = User.find_by(referral_code: code)
|
|
340
|
+
if referrer
|
|
341
|
+
result.with_context(:referrer, referrer).continue(result.value)
|
|
342
|
+
else
|
|
343
|
+
result.halt.with_error(:referral, "Invalid referral code")
|
|
344
|
+
end
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Sanitization and Normalization
|
|
349
|
+
|
|
350
|
+
### Data Cleaning
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
step :sanitize_input, ->(result) {
|
|
354
|
+
data = result.value
|
|
355
|
+
|
|
356
|
+
sanitized = {
|
|
357
|
+
name: data[:name]&.strip&.gsub(/\s+/, ' '),
|
|
358
|
+
email: data[:email]&.downcase&.strip,
|
|
359
|
+
phone: data[:phone]&.gsub(/[^\d+]/, ''),
|
|
360
|
+
bio: data[:bio]&.strip&.slice(0, 500)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
result.continue(sanitized)
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Data Normalization
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
step :normalize_address, ->(result) {
|
|
371
|
+
address = result.value
|
|
372
|
+
|
|
373
|
+
normalized = {
|
|
374
|
+
street: address[:street]&.upcase,
|
|
375
|
+
city: address[:city]&.titleize,
|
|
376
|
+
state: address[:state]&.upcase,
|
|
377
|
+
zip: address[:zip]&.gsub(/[^\d-]/, ''),
|
|
378
|
+
country: address[:country]&.upcase
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
result.continue(normalized)
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Validation Middleware
|
|
386
|
+
|
|
387
|
+
### Automatic Validation Middleware
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
class ValidationMiddleware
|
|
391
|
+
def initialize(callable, validator:)
|
|
392
|
+
@callable = callable
|
|
393
|
+
@validator = validator
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def call(result)
|
|
397
|
+
validation_result = @validator.call(result)
|
|
398
|
+
|
|
399
|
+
if validation_result.errors.any?
|
|
400
|
+
validation_result.halt(validation_result.value)
|
|
401
|
+
else
|
|
402
|
+
@callable.call(validation_result)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
408
|
+
use_middleware ValidationMiddleware, validator: EmailValidator
|
|
409
|
+
|
|
410
|
+
step ->(result) {
|
|
411
|
+
# Only executes if email validation passes
|
|
412
|
+
result.continue("Email validated: #{result.value[:email]}")
|
|
413
|
+
}
|
|
414
|
+
end
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Complete Example
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
class UserRegistrationPipeline
|
|
421
|
+
def self.build
|
|
422
|
+
SimpleFlow::Pipeline.new do
|
|
423
|
+
# Sanitize inputs
|
|
424
|
+
step :sanitize, ->(result) {
|
|
425
|
+
data = result.value
|
|
426
|
+
sanitized = {
|
|
427
|
+
name: data[:name]&.strip,
|
|
428
|
+
email: data[:email]&.downcase&.strip,
|
|
429
|
+
phone: data[:phone]&.gsub(/[^\d+]/, ''),
|
|
430
|
+
password: data[:password]
|
|
431
|
+
}
|
|
432
|
+
result.continue(sanitized)
|
|
433
|
+
}, depends_on: []
|
|
434
|
+
|
|
435
|
+
# Parallel validations
|
|
436
|
+
step :validate_name, ->(result) {
|
|
437
|
+
if result.value[:name].nil? || result.value[:name].empty?
|
|
438
|
+
result.with_error(:name, "Name is required")
|
|
439
|
+
else
|
|
440
|
+
result.continue(result.value)
|
|
441
|
+
end
|
|
442
|
+
}, depends_on: [:sanitize]
|
|
443
|
+
|
|
444
|
+
step :validate_email, EmailValidator, depends_on: [:sanitize]
|
|
445
|
+
step :validate_password, PasswordValidator, depends_on: [:sanitize]
|
|
446
|
+
step :validate_phone, ->(result) {
|
|
447
|
+
phone = result.value[:phone]
|
|
448
|
+
unless phone =~ /\A\+?\d{10,15}\z/
|
|
449
|
+
result.with_error(:phone, "Invalid phone format")
|
|
450
|
+
end
|
|
451
|
+
result.continue(result.value)
|
|
452
|
+
}, depends_on: [:sanitize]
|
|
453
|
+
|
|
454
|
+
# Check uniqueness
|
|
455
|
+
step :check_uniqueness, ->(result) {
|
|
456
|
+
if User.exists?(email: result.value[:email])
|
|
457
|
+
result.with_error(:email, "Email already registered")
|
|
458
|
+
end
|
|
459
|
+
result.continue(result.value)
|
|
460
|
+
}, depends_on: [:validate_email]
|
|
461
|
+
|
|
462
|
+
# Verify all validations passed
|
|
463
|
+
step :verify, ->(result) {
|
|
464
|
+
if result.errors.any?
|
|
465
|
+
result.halt(result.value)
|
|
466
|
+
else
|
|
467
|
+
result.continue(result.value)
|
|
468
|
+
end
|
|
469
|
+
}, depends_on: [:validate_name, :validate_email, :validate_password, :validate_phone, :check_uniqueness]
|
|
470
|
+
|
|
471
|
+
# Create user
|
|
472
|
+
step :create_user, ->(result) {
|
|
473
|
+
user = User.create!(result.value)
|
|
474
|
+
result.continue(user)
|
|
475
|
+
}, depends_on: [:verify]
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Usage
|
|
481
|
+
result = UserRegistrationPipeline.build.call_parallel(
|
|
482
|
+
SimpleFlow::Result.new(user_params)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if result.continue?
|
|
486
|
+
redirect_to dashboard_path, notice: "Welcome!"
|
|
487
|
+
else
|
|
488
|
+
render :new, errors: result.errors
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Related Documentation
|
|
493
|
+
|
|
494
|
+
- [Error Handling](error-handling.md) - Comprehensive error handling strategies
|
|
495
|
+
- [Complex Workflows](complex-workflows.md) - Building sophisticated pipelines
|
|
496
|
+
- [Result API](../api/result.md) - Result class reference
|
data/docs/index.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# SimpleFlow
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
[](https://badge.fury.io/rb/simple_flow)
|
|
8
|
+
[](https://github.com/MadBomber/simple_flow/actions)
|
|
9
|
+
[](https://www.ruby-lang.org/)
|
|
10
|
+
|
|
11
|
+
**A lightweight, modular Ruby framework for building composable data processing pipelines with concurrent execution.**
|
|
12
|
+
|
|
13
|
+
[Get Started](getting-started/quick-start.md){ .md-button .md-button--primary }
|
|
14
|
+
[View on GitHub](https://github.com/MadBomber/simple_flow){ .md-button }
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
SimpleFlow provides a clean and flexible architecture for orchestrating multi-step workflows. It emphasizes simplicity, composability, and performance through fiber-based concurrent execution.
|
|
23
|
+
|
|
24
|
+
## Key Features
|
|
25
|
+
|
|
26
|
+
### 🔄 Concurrent Execution
|
|
27
|
+
Run independent steps in parallel using the Async gem for significant performance improvements.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
31
|
+
parallel do
|
|
32
|
+
step ->(result) { fetch_orders(result) }
|
|
33
|
+
step ->(result) { fetch_preferences(result) }
|
|
34
|
+
step ->(result) { fetch_analytics(result) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 🔗 Composable Pipelines
|
|
40
|
+
Build complex workflows from simple, reusable steps with an intuitive DSL.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
44
|
+
step ->(result) { validate(result) }
|
|
45
|
+
step ->(result) { transform(result) }
|
|
46
|
+
step ->(result) { save(result) }
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 🛡️ Immutable Results
|
|
51
|
+
Thread-safe result objects with context and error tracking throughout the pipeline.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
result = SimpleFlow::Result.new(data)
|
|
55
|
+
.with_context(:user_id, 123)
|
|
56
|
+
.with_error(:validation, "Invalid format")
|
|
57
|
+
.continue(processed_data)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 🔌 Middleware Support
|
|
61
|
+
Apply cross-cutting concerns like logging and instrumentation to all steps.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
65
|
+
use_middleware SimpleFlow::MiddleWare::Logging
|
|
66
|
+
use_middleware SimpleFlow::MiddleWare::Instrumentation
|
|
67
|
+
|
|
68
|
+
step ->(result) { process(result) }
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### ⚡ Flow Control
|
|
73
|
+
Halt execution early or continue based on step outcomes with built-in mechanisms.
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
step ->(result) {
|
|
77
|
+
if result.value < 0
|
|
78
|
+
result.halt.with_error(:validation, "Value must be positive")
|
|
79
|
+
else
|
|
80
|
+
result.continue(result.value)
|
|
81
|
+
end
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 📊 Built for Performance
|
|
86
|
+
Fiber-based concurrency without threading overhead, ideal for I/O-bound operations.
|
|
87
|
+
|
|
88
|
+
**Performance Example:**
|
|
89
|
+
- Sequential: ~0.4s (4 × 0.1s operations)
|
|
90
|
+
- Parallel: ~0.1s (4 concurrent operations)
|
|
91
|
+
- **4x speedup!**
|
|
92
|
+
|
|
93
|
+
## Quick Example
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
require 'simple_flow'
|
|
97
|
+
|
|
98
|
+
# Build a user data pipeline
|
|
99
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
100
|
+
step ->(result) { validate_user(result) }
|
|
101
|
+
|
|
102
|
+
parallel do
|
|
103
|
+
step ->(result) { fetch_profile(result) }
|
|
104
|
+
step ->(result) { fetch_orders(result) }
|
|
105
|
+
step ->(result) { fetch_analytics(result) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
step ->(result) { aggregate_data(result) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
result = pipeline.call(SimpleFlow::Result.new(user_id: 123))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Why SimpleFlow?
|
|
115
|
+
|
|
116
|
+
- **Simple**: Minimal API surface, maximum power
|
|
117
|
+
- **Fast**: Fiber-based concurrency for I/O-bound operations
|
|
118
|
+
- **Safe**: Immutable results prevent race conditions
|
|
119
|
+
- **Flexible**: Middleware and flow control for any use case
|
|
120
|
+
- **Testable**: Easy to unit test individual steps
|
|
121
|
+
- **Production-Ready**: Used in real-world applications
|
|
122
|
+
|
|
123
|
+
## Next Steps
|
|
124
|
+
|
|
125
|
+
<div class="grid cards" markdown>
|
|
126
|
+
|
|
127
|
+
- :material-clock-fast:{ .lg .middle } __Quick Start__
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
Get up and running in 5 minutes
|
|
132
|
+
|
|
133
|
+
[:octicons-arrow-right-24: Quick Start](getting-started/quick-start.md)
|
|
134
|
+
|
|
135
|
+
- :material-book-open-variant:{ .lg .middle } __Core Concepts__
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
Learn the fundamental concepts
|
|
140
|
+
|
|
141
|
+
[:octicons-arrow-right-24: Core Concepts](core-concepts/overview.md)
|
|
142
|
+
|
|
143
|
+
- :material-lightning-bolt:{ .lg .middle } __Concurrent Execution__
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
Maximize performance with parallel steps
|
|
148
|
+
|
|
149
|
+
[:octicons-arrow-right-24: Concurrency Guide](concurrent/introduction.md)
|
|
150
|
+
|
|
151
|
+
- :material-code-braces:{ .lg .middle } __Examples__
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
Real-world examples and patterns
|
|
156
|
+
|
|
157
|
+
[:octicons-arrow-right-24: Examples](getting-started/examples.md)
|
|
158
|
+
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
## Community & Support
|
|
162
|
+
|
|
163
|
+
- :fontawesome-brands-github: [GitHub Repository](https://github.com/MadBomber/simple_flow)
|
|
164
|
+
- :material-bug: [Issue Tracker](https://github.com/MadBomber/simple_flow/issues)
|
|
165
|
+
- :material-file-document: [Changelog](https://github.com/MadBomber/simple_flow/blob/main/CHANGELOG.md)
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
SimpleFlow is released under the [MIT License](https://github.com/MadBomber/simple_flow/blob/main/LICENSE).
|
data/examples/.gitignore
ADDED