piperb 0.1.1
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/.rspec +3 -0
- data/.rubocop.yml +78 -0
- data/LICENSE.txt +21 -0
- data/README.md +353 -0
- data/Rakefile +10 -0
- data/claude.md +346 -0
- data/coverage/.last_run.json +6 -0
- data/coverage/.resultset.json +1500 -0
- data/coverage/.resultset.json.lock +0 -0
- data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc.png +0 -0
- data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
- data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_both.png +0 -0
- data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc.png +0 -0
- data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
- data/coverage/assets/0.13.2/application.css +1 -0
- data/coverage/assets/0.13.2/application.js +7 -0
- data/coverage/assets/0.13.2/colorbox/border.png +0 -0
- data/coverage/assets/0.13.2/colorbox/controls.png +0 -0
- data/coverage/assets/0.13.2/colorbox/loading.gif +0 -0
- data/coverage/assets/0.13.2/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.13.2/favicon_green.png +0 -0
- data/coverage/assets/0.13.2/favicon_red.png +0 -0
- data/coverage/assets/0.13.2/favicon_yellow.png +0 -0
- data/coverage/assets/0.13.2/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.13.2/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.13.2/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.13.2/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.13.2/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.13.2/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.13.2/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.13.2/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.13.2/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.13.2/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.13.2/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.13.2/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.13.2/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.13.2/loading.gif +0 -0
- data/coverage/assets/0.13.2/magnify.png +0 -0
- data/coverage/index.html +16329 -0
- data/lib/piperb/cache.rb +121 -0
- data/lib/piperb/dag.rb +196 -0
- data/lib/piperb/errors.rb +61 -0
- data/lib/piperb/executor/base.rb +244 -0
- data/lib/piperb/executor/parallel.rb +127 -0
- data/lib/piperb/executor/sequential.rb +79 -0
- data/lib/piperb/pipeline.rb +90 -0
- data/lib/piperb/result.rb +116 -0
- data/lib/piperb/step.rb +92 -0
- data/lib/piperb/version.rb +5 -0
- data/lib/piperb.rb +39 -0
- data/piperb.gemspec +33 -0
- metadata +99 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 29ceb7d0f30eeef6c0386ddfb3530ddcb51cf1f76d8fdc9d6c51d98e9305bbc2
|
|
4
|
+
data.tar.gz: 4bde8fce73584804ae69b1b8dafb84a508050b4934d5ce24a50b975a272f7b8d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 22186fa4ca372d5aa9419e60dd6503cc839e73c381ecb6b098969d3cebb62253afbb541575e9e0e7f475916c183c188644d68f1b61688cc4df7b11d256890e98
|
|
7
|
+
data.tar.gz: 4f89a72e832278b4adfc79e38f2303f9574224cb14c2f28718d46dc190b3866dca2994c1710cbd6f28f953dc12ffd934cbee8ec0b2a704bd15db4cf4b142e7f8
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
|
|
4
|
+
AllCops:
|
|
5
|
+
TargetRubyVersion: 3.1
|
|
6
|
+
NewCops: enable
|
|
7
|
+
SuggestExtensions: false
|
|
8
|
+
Exclude:
|
|
9
|
+
- 'vendor/**/*'
|
|
10
|
+
- 'bin/**/*'
|
|
11
|
+
|
|
12
|
+
Style/Documentation:
|
|
13
|
+
Enabled: false
|
|
14
|
+
|
|
15
|
+
Style/FrozenStringLiteralComment:
|
|
16
|
+
Enabled: true
|
|
17
|
+
|
|
18
|
+
Style/MultilineBlockChain:
|
|
19
|
+
Enabled: false
|
|
20
|
+
|
|
21
|
+
Metrics/BlockLength:
|
|
22
|
+
Exclude:
|
|
23
|
+
- 'spec/**/*'
|
|
24
|
+
- 'flowrb.gemspec'
|
|
25
|
+
|
|
26
|
+
Metrics/MethodLength:
|
|
27
|
+
Max: 30
|
|
28
|
+
|
|
29
|
+
Metrics/AbcSize:
|
|
30
|
+
Max: 30
|
|
31
|
+
|
|
32
|
+
Metrics/ClassLength:
|
|
33
|
+
Max: 150
|
|
34
|
+
|
|
35
|
+
Metrics/ParameterLists:
|
|
36
|
+
Max: 8
|
|
37
|
+
|
|
38
|
+
Layout/LineLength:
|
|
39
|
+
Max: 120
|
|
40
|
+
Exclude:
|
|
41
|
+
- 'spec/**/*'
|
|
42
|
+
- 'flowrb.gemspec'
|
|
43
|
+
|
|
44
|
+
Naming/PredicateMethod:
|
|
45
|
+
Enabled: false
|
|
46
|
+
|
|
47
|
+
Naming/VariableNumber:
|
|
48
|
+
Exclude:
|
|
49
|
+
- 'spec/**/*'
|
|
50
|
+
|
|
51
|
+
Naming/MethodParameterName:
|
|
52
|
+
Exclude:
|
|
53
|
+
- 'spec/**/*'
|
|
54
|
+
|
|
55
|
+
RSpec/SpecFilePathFormat:
|
|
56
|
+
Enabled: false
|
|
57
|
+
|
|
58
|
+
Lint/EmptyBlock:
|
|
59
|
+
Exclude:
|
|
60
|
+
- 'spec/**/*'
|
|
61
|
+
|
|
62
|
+
RSpec/MultipleExpectations:
|
|
63
|
+
Enabled: false
|
|
64
|
+
|
|
65
|
+
RSpec/MultipleDescribes:
|
|
66
|
+
Enabled: false
|
|
67
|
+
|
|
68
|
+
RSpec/ExampleLength:
|
|
69
|
+
Enabled: false
|
|
70
|
+
|
|
71
|
+
RSpec/NestedGroups:
|
|
72
|
+
Max: 4
|
|
73
|
+
|
|
74
|
+
RSpec/DescribeClass:
|
|
75
|
+
Exclude:
|
|
76
|
+
- 'spec/integration/**/*'
|
|
77
|
+
- 'spec/flowrb/step_retry_spec.rb'
|
|
78
|
+
- 'spec/flowrb/step_timeout_spec.rb'
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Flowline Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# Piperb
|
|
2
|
+
|
|
3
|
+
A Ruby dataflow and pipeline library with declarative step definitions, automatic dependency resolution, parallel/sequential execution, and built-in retry/timeout support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'piperb'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install piperb
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **Declarative step definitions** - Define what each step does, not how to orchestrate them
|
|
28
|
+
- **Automatic dependency resolution** - Steps execute in the correct order based on dependencies
|
|
29
|
+
- **Parallel execution** - Independent steps run concurrently using threads
|
|
30
|
+
- **Retries with backoff** - Automatic retry with linear or exponential backoff strategies
|
|
31
|
+
- **Timeouts** - Per-step execution time limits
|
|
32
|
+
- **Conditional execution** - Skip steps based on runtime conditions
|
|
33
|
+
- **Luigi-style caching** - Resume failed pipelines from the last successful step
|
|
34
|
+
- **Zero runtime dependencies** - Pure Ruby using only stdlib
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require 'piperb'
|
|
40
|
+
|
|
41
|
+
pipeline = Piperb.define do
|
|
42
|
+
step :fetch do
|
|
43
|
+
[1, 2, 3]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
step :transform, depends_on: :fetch do |data|
|
|
47
|
+
data.map { |n| n * 2 }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
step :load, depends_on: :transform do |data|
|
|
51
|
+
data.sum
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
result = pipeline.run
|
|
56
|
+
result.success? # => true
|
|
57
|
+
result[:load].output # => 12
|
|
58
|
+
result[:load].duration # => 0.001
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
### Basic Pipeline
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
pipeline = Piperb.define do
|
|
67
|
+
step :fetch_users do
|
|
68
|
+
User.all.to_a
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
step :enrich, depends_on: :fetch_users do |users|
|
|
72
|
+
users.map { |u| enrich_from_api(u) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
step :export_csv, depends_on: :enrich do |users|
|
|
76
|
+
CSV.generate { |csv| users.each { |u| csv << u.to_a } }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
step :export_json, depends_on: :enrich do |users|
|
|
80
|
+
users.to_json
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Multiple dependencies - outputs passed as keyword arguments
|
|
84
|
+
step :notify, depends_on: [:export_csv, :export_json] do |export_csv:, export_json:|
|
|
85
|
+
Notifier.send("Exported #{export_csv.lines.count} rows")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
result = pipeline.run
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Parallel Execution
|
|
93
|
+
|
|
94
|
+
Steps at the same "level" (no inter-dependencies) run concurrently:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
pipeline = Piperb.define do
|
|
98
|
+
step :fetch_users do
|
|
99
|
+
fetch_from_api("/users")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
step :fetch_orders do
|
|
103
|
+
fetch_from_api("/orders")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# fetch_users and fetch_orders run in parallel
|
|
107
|
+
|
|
108
|
+
step :generate_report, depends_on: [:fetch_users, :fetch_orders] do |fetch_users:, fetch_orders:|
|
|
109
|
+
{ users: fetch_users, orders: fetch_orders }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Parallel execution
|
|
114
|
+
result = pipeline.run(executor: :parallel)
|
|
115
|
+
|
|
116
|
+
# Parallel with thread limit
|
|
117
|
+
result = pipeline.run(executor: :parallel, max_threads: 4)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Step Retries
|
|
121
|
+
|
|
122
|
+
Steps can be configured to automatically retry on failure:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
pipeline = Piperb.define do
|
|
126
|
+
step :fetch_api, retries: 3, retry_delay: 2 do
|
|
127
|
+
HTTP.get("https://api.example.com/data")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Exponential backoff: delays of 1s, 2s, 4s
|
|
131
|
+
step :flaky_service, retries: 3, retry_delay: 1, retry_backoff: :exponential do
|
|
132
|
+
ExternalService.call
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Linear backoff: delays of 1s, 2s, 3s
|
|
136
|
+
step :another_service, retries: 3, retry_delay: 1, retry_backoff: :linear do
|
|
137
|
+
AnotherService.call
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Conditional retry - only retry on specific errors
|
|
141
|
+
step :selective_retry, retries: 3, retry_if: ->(error) { error.is_a?(IOError) } do
|
|
142
|
+
risky_operation
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
result = pipeline.run
|
|
147
|
+
result[:fetch_api].retries # => number of retries that occurred
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Retry Options:**
|
|
151
|
+
- `retries: n` - Maximum retry attempts (default: 0)
|
|
152
|
+
- `retry_delay: seconds` - Wait time between retries (default: 0)
|
|
153
|
+
- `retry_backoff: :exponential | :linear` - Backoff strategy for delays
|
|
154
|
+
- `retry_if: ->(error) { ... }` - Only retry if condition returns true
|
|
155
|
+
|
|
156
|
+
### Step Timeouts
|
|
157
|
+
|
|
158
|
+
Steps can be configured with execution timeouts:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
pipeline = Piperb.define do
|
|
162
|
+
step :slow_operation, timeout: 30 do
|
|
163
|
+
# Will raise TimeoutError if not complete in 30 seconds
|
|
164
|
+
long_running_computation
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Combine timeout with retries
|
|
168
|
+
step :unreliable, timeout: 10, retries: 3, retry_delay: 5 do
|
|
169
|
+
external_api_call
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
result = pipeline.run
|
|
174
|
+
result[:slow_operation].timed_out? # => true if step timed out
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Conditional Execution
|
|
178
|
+
|
|
179
|
+
Steps can be conditionally executed based on runtime conditions:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
pipeline = Piperb.define do
|
|
183
|
+
step :config do
|
|
184
|
+
{ feature_enabled: true, skip_export: false }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Only runs when if: condition returns truthy
|
|
188
|
+
step :feature_a, depends_on: :config, if: ->(cfg) { cfg[:feature_enabled] } do |cfg|
|
|
189
|
+
'feature A result'
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Skipped when unless: condition returns truthy
|
|
193
|
+
step :export, depends_on: :config, unless: ->(cfg) { cfg[:skip_export] } do |cfg|
|
|
194
|
+
'export result'
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Handles nil from skipped dependency
|
|
198
|
+
step :finalize, depends_on: :feature_a do |input|
|
|
199
|
+
input.nil? ? 'dependency was skipped' : "got: #{input}"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
result = pipeline.run
|
|
204
|
+
result[:feature_a].skipped? # => false
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Conditional Behavior:**
|
|
208
|
+
- `if: condition` - Runs step only when condition returns truthy
|
|
209
|
+
- `unless: condition` - Skips step when condition returns truthy
|
|
210
|
+
- Skipped steps return `nil` output with `:skipped` status
|
|
211
|
+
- Dependent steps receive `nil` for skipped dependency outputs
|
|
212
|
+
- Skipped steps don't count as failures (pipeline still succeeds)
|
|
213
|
+
|
|
214
|
+
### Caching (Luigi-style)
|
|
215
|
+
|
|
216
|
+
Steps can be cached to enable resuming failed pipelines from the last successful step:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# Using a file-based cache (persists across runs)
|
|
220
|
+
pipeline.run(cache: './cache')
|
|
221
|
+
|
|
222
|
+
# Using a memory cache (for testing)
|
|
223
|
+
cache = Piperb::Cache::MemoryStore.new
|
|
224
|
+
pipeline.run(cache: cache)
|
|
225
|
+
|
|
226
|
+
# Force re-execution (ignores cache)
|
|
227
|
+
pipeline.run(cache: './cache', force: true)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### Step-level Cache Control
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
pipeline = Piperb.define do
|
|
234
|
+
# This step is cached (default behavior)
|
|
235
|
+
step :fetch_data do
|
|
236
|
+
expensive_api_call
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# This step is never cached
|
|
240
|
+
step :current_time, cache: false do
|
|
241
|
+
Time.now
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Custom cache key based on input
|
|
245
|
+
step :process, depends_on: :fetch_data, cache_key: ->(input) { "process_#{input[:id]}" } do |data|
|
|
246
|
+
transform(data)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### Resume Failed Pipeline
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# First run - step 2 fails, but step 1 is cached
|
|
255
|
+
begin
|
|
256
|
+
pipeline.run(cache: './cache')
|
|
257
|
+
rescue Piperb::StepError
|
|
258
|
+
puts "Pipeline failed, but progress was saved"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Second run - step 1 loads from cache, step 2 retries
|
|
262
|
+
result = pipeline.run(cache: './cache')
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Cache Options:**
|
|
266
|
+
- `cache: path` - File-based cache directory
|
|
267
|
+
- `cache: store` - Custom cache store implementing `Piperb::Cache::Base`
|
|
268
|
+
- `force: true` - Ignore cache and re-execute all steps
|
|
269
|
+
- Step option `cache: false` - Disable caching for specific steps
|
|
270
|
+
- Step option `cache_key: lambda` - Custom cache key based on input
|
|
271
|
+
|
|
272
|
+
### Input Passing Strategy
|
|
273
|
+
|
|
274
|
+
- **No dependencies**: receives `initial_input` or empty args
|
|
275
|
+
- **Single dependency**: output passed directly as argument
|
|
276
|
+
- **Multiple dependencies**: outputs passed as keyword arguments
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
# Single dependency - direct argument
|
|
280
|
+
step :process, depends_on: :fetch do |data|
|
|
281
|
+
data.map(&:transform)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Multiple dependencies - keyword arguments
|
|
285
|
+
step :merge, depends_on: [:csv, :json] do |csv:, json:|
|
|
286
|
+
{ csv: csv, json: json }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Initial input
|
|
290
|
+
result = pipeline.run(initial_input: { date: Date.today })
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Mermaid Diagram Generation
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
pipeline = Piperb.define do
|
|
297
|
+
step :fetch do; end
|
|
298
|
+
step :process, depends_on: :fetch do |_|; end
|
|
299
|
+
step :save, depends_on: :process do |_|; end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
puts pipeline.to_mermaid
|
|
303
|
+
# graph TD
|
|
304
|
+
# fetch --> process
|
|
305
|
+
# process --> save
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Error Handling
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
Piperb::Error # Base error
|
|
312
|
+
Piperb::CycleError # Circular dependency detected
|
|
313
|
+
Piperb::MissingDependencyError # Unknown dependency referenced
|
|
314
|
+
Piperb::DuplicateStepError # Step name already exists
|
|
315
|
+
Piperb::StepError # Step execution failed
|
|
316
|
+
Piperb::TimeoutError # Step exceeded timeout duration
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`StepError` wraps the original error and includes partial results:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
begin
|
|
323
|
+
pipeline.run
|
|
324
|
+
rescue Piperb::StepError => e
|
|
325
|
+
e.step_name # => :failed_step
|
|
326
|
+
e.original_error # => the underlying exception
|
|
327
|
+
e.partial_results # => results from completed steps
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Development
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
bundle install
|
|
335
|
+
bundle exec rspec # Run tests (597 examples)
|
|
336
|
+
bundle exec rubocop # Run linter
|
|
337
|
+
bundle exec rake # Run both tests and linter
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Test Coverage
|
|
341
|
+
|
|
342
|
+
- Line Coverage: ~97%
|
|
343
|
+
- Branch Coverage: ~90%
|
|
344
|
+
- 597 test examples
|
|
345
|
+
|
|
346
|
+
## Requirements
|
|
347
|
+
|
|
348
|
+
- Ruby >= 3.1.0
|
|
349
|
+
- No runtime dependencies (pure Ruby, stdlib only)
|
|
350
|
+
|
|
351
|
+
## License
|
|
352
|
+
|
|
353
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|