igniter 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +224 -1
- data/docs/API_V2.md +238 -1
- data/docs/BACKLOG.md +166 -0
- data/docs/BRANCHES_V1.md +213 -0
- data/docs/COLLECTIONS_V1.md +303 -0
- data/docs/EXECUTION_MODEL_V2.md +79 -0
- data/docs/PATTERNS.md +222 -0
- data/docs/STORE_ADAPTERS.md +126 -0
- data/examples/README.md +124 -0
- data/examples/async_store.rb +47 -0
- data/examples/collection.rb +43 -0
- data/examples/collection_partial_failure.rb +50 -0
- data/examples/marketing_ergonomics.rb +57 -0
- data/examples/ringcentral_routing.rb +278 -0
- data/lib/igniter/compiler/compiled_graph.rb +82 -0
- data/lib/igniter/compiler/graph_compiler.rb +12 -2
- data/lib/igniter/compiler/type_resolver.rb +54 -0
- data/lib/igniter/compiler/validation_context.rb +61 -0
- data/lib/igniter/compiler/validation_pipeline.rb +30 -0
- data/lib/igniter/compiler/validator.rb +1 -187
- data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
- data/lib/igniter/compiler/validators/dependencies_validator.rb +151 -0
- data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
- data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
- data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
- data/lib/igniter/compiler.rb +8 -0
- data/lib/igniter/contract.rb +136 -4
- data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
- data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
- data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
- data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
- data/lib/igniter/diagnostics/report.rb +84 -8
- data/lib/igniter/dsl/contract_builder.rb +208 -5
- data/lib/igniter/dsl/schema_builder.rb +73 -0
- data/lib/igniter/dsl.rb +1 -0
- data/lib/igniter/errors.rb +11 -0
- data/lib/igniter/events/bus.rb +5 -0
- data/lib/igniter/events/event.rb +29 -0
- data/lib/igniter/executor.rb +74 -0
- data/lib/igniter/executor_registry.rb +44 -0
- data/lib/igniter/extensions/auditing/timeline.rb +4 -0
- data/lib/igniter/extensions/introspection/graph_formatter.rb +29 -3
- data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
- data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
- data/lib/igniter/extensions/introspection.rb +1 -0
- data/lib/igniter/extensions/reactive/engine.rb +49 -2
- data/lib/igniter/extensions/reactive/reaction.rb +3 -2
- data/lib/igniter/model/branch_node.rb +40 -0
- data/lib/igniter/model/collection_node.rb +25 -0
- data/lib/igniter/model/composition_node.rb +2 -2
- data/lib/igniter/model/compute_node.rb +58 -2
- data/lib/igniter/model/input_node.rb +2 -2
- data/lib/igniter/model/output_node.rb +24 -4
- data/lib/igniter/model.rb +2 -0
- data/lib/igniter/runtime/cache.rb +64 -25
- data/lib/igniter/runtime/collection_result.rb +111 -0
- data/lib/igniter/runtime/deferred_result.rb +40 -0
- data/lib/igniter/runtime/execution.rb +261 -11
- data/lib/igniter/runtime/input_validator.rb +2 -24
- data/lib/igniter/runtime/invalidator.rb +1 -1
- data/lib/igniter/runtime/job_worker.rb +18 -0
- data/lib/igniter/runtime/node_state.rb +20 -0
- data/lib/igniter/runtime/planner.rb +126 -0
- data/lib/igniter/runtime/resolver.rb +269 -15
- data/lib/igniter/runtime/result.rb +14 -2
- data/lib/igniter/runtime/runner_factory.rb +20 -0
- data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
- data/lib/igniter/runtime/runners/store_runner.rb +29 -0
- data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
- data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
- data/lib/igniter/runtime/stores/file_store.rb +43 -0
- data/lib/igniter/runtime/stores/memory_store.rb +40 -0
- data/lib/igniter/runtime/stores/redis_store.rb +44 -0
- data/lib/igniter/runtime.rb +12 -0
- data/lib/igniter/type_system.rb +44 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +23 -0
- metadata +43 -2
data/docs/EXECUTION_MODEL_V2.md
CHANGED
|
@@ -162,6 +162,8 @@ Canonical kernel events:
|
|
|
162
162
|
- `node_started`
|
|
163
163
|
- `node_succeeded`
|
|
164
164
|
- `node_failed`
|
|
165
|
+
- `node_pending`
|
|
166
|
+
- `node_resumed`
|
|
165
167
|
- `node_invalidated`
|
|
166
168
|
|
|
167
169
|
Suggested event fields:
|
|
@@ -181,6 +183,7 @@ Current payload examples:
|
|
|
181
183
|
- composition success payload includes `child_execution_id` and `child_graph`
|
|
182
184
|
- `execution_failed` includes `graph`, `targets`, and `error`
|
|
183
185
|
- `node_invalidated` includes `cause`
|
|
186
|
+
- `node_pending` includes deferred token/payload
|
|
184
187
|
|
|
185
188
|
## Public Resolution API
|
|
186
189
|
|
|
@@ -215,6 +218,80 @@ Reasons:
|
|
|
215
218
|
|
|
216
219
|
Thread-safe or parallel execution can be added later behind explicit executors.
|
|
217
220
|
|
|
221
|
+
Current core now supports:
|
|
222
|
+
|
|
223
|
+
- `:inline` runner
|
|
224
|
+
- `:thread_pool` runner
|
|
225
|
+
- `:store` runner for pending snapshot persistence
|
|
226
|
+
|
|
227
|
+
## Pending And Resume
|
|
228
|
+
|
|
229
|
+
Executors may return a deferred value instead of a final result:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
class AsyncQuoteExecutor < Igniter::Executor
|
|
233
|
+
def call(order_total:)
|
|
234
|
+
defer(token: "quote-#{order_total}", payload: { kind: "pricing_quote" })
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Runtime behavior:
|
|
240
|
+
|
|
241
|
+
1. node resolves to `:pending`
|
|
242
|
+
2. a `DeferredResult` is stored in cache
|
|
243
|
+
3. downstream nodes that depend on it also resolve to `:pending`
|
|
244
|
+
4. caller may persist a snapshot
|
|
245
|
+
5. later, runtime resumes the source node with a final value
|
|
246
|
+
|
|
247
|
+
Public resume API:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
contract.execution.resume(:quote_total, value: 150)
|
|
251
|
+
contract.execution.resume_by_token("quote-100", value: 150)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Snapshot And Store Flow
|
|
255
|
+
|
|
256
|
+
Execution snapshot contains:
|
|
257
|
+
|
|
258
|
+
- graph name
|
|
259
|
+
- execution id
|
|
260
|
+
- runner metadata
|
|
261
|
+
- normalized inputs
|
|
262
|
+
- serialized cache states
|
|
263
|
+
- serialized events
|
|
264
|
+
|
|
265
|
+
Current store implementations:
|
|
266
|
+
|
|
267
|
+
- `Igniter::Runtime::Stores::MemoryStore`
|
|
268
|
+
- `Igniter::Runtime::Stores::FileStore`
|
|
269
|
+
|
|
270
|
+
Store-backed flow:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
class AsyncPricingContract < Igniter::Contract
|
|
274
|
+
run_with runner: :store
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
contract = AsyncPricingContract.new(order_total: 100)
|
|
278
|
+
deferred = contract.result.gross_total
|
|
279
|
+
|
|
280
|
+
execution_id = contract.execution.events.execution_id
|
|
281
|
+
restored = AsyncPricingContract.restore_from_store(execution_id)
|
|
282
|
+
restored.execution.resume_by_token(deferred.token, value: 150)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Worker entrypoint:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
AsyncPricingContract.resume_from_store(
|
|
289
|
+
execution_id,
|
|
290
|
+
token: deferred.token,
|
|
291
|
+
value: 150
|
|
292
|
+
)
|
|
293
|
+
```
|
|
294
|
+
|
|
218
295
|
## Kernel Invariants
|
|
219
296
|
|
|
220
297
|
These invariants should be enforced by tests:
|
|
@@ -227,6 +304,8 @@ These invariants should be enforced by tests:
|
|
|
227
304
|
6. event order is deterministic
|
|
228
305
|
7. failures remain inspectable in cache/result
|
|
229
306
|
8. composition creates isolated child executions
|
|
307
|
+
9. pending nodes are not treated as succeeded
|
|
308
|
+
10. restored executions preserve pending tokens and event identity
|
|
230
309
|
|
|
231
310
|
## Testing Strategy
|
|
232
311
|
|
data/docs/PATTERNS.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Igniter Patterns
|
|
2
|
+
|
|
3
|
+
This document collects recommended orchestration shapes that already have runnable examples in the repository.
|
|
4
|
+
|
|
5
|
+
The goal is not to introduce new primitives, but to show how the existing DSL composes into readable contracts.
|
|
6
|
+
|
|
7
|
+
## 1. Linear Derivation
|
|
8
|
+
|
|
9
|
+
Use this when the flow is a straight dependency chain with no routing or fan-out.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
|
|
13
|
+
- [basic_pricing.rb](/Users/alex/dev/hotfix/igniter/examples/basic_pricing.rb)
|
|
14
|
+
|
|
15
|
+
Use:
|
|
16
|
+
|
|
17
|
+
- pricing
|
|
18
|
+
- totals
|
|
19
|
+
- normalization pipelines
|
|
20
|
+
- simple business formulas
|
|
21
|
+
|
|
22
|
+
Shape:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
input :order_total
|
|
26
|
+
input :country
|
|
27
|
+
|
|
28
|
+
compute :vat_rate, with: :country do |country:|
|
|
29
|
+
country == "UA" ? 0.2 : 0.0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
compute :gross_total, with: %i[order_total vat_rate] do |order_total:, vat_rate:|
|
|
33
|
+
order_total * (1 + vat_rate)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
output :gross_total
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 2. Stage Composition
|
|
40
|
+
|
|
41
|
+
Use this when one contract should orchestrate several bounded subgraphs.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
|
|
45
|
+
- [composition.rb](/Users/alex/dev/hotfix/igniter/examples/composition.rb)
|
|
46
|
+
- [ringcentral_routing.rb](/Users/alex/dev/hotfix/igniter/examples/ringcentral_routing.rb)
|
|
47
|
+
|
|
48
|
+
Use:
|
|
49
|
+
|
|
50
|
+
- reusable business stages
|
|
51
|
+
- transport shell + domain pipeline
|
|
52
|
+
- larger flows with clear internal boundaries
|
|
53
|
+
|
|
54
|
+
Guideline:
|
|
55
|
+
|
|
56
|
+
- keep the parent contract thin
|
|
57
|
+
- export child outputs explicitly
|
|
58
|
+
- read child diagnostics from the child execution, not from the parent by default
|
|
59
|
+
|
|
60
|
+
## 3. Scoped Domain Flow
|
|
61
|
+
|
|
62
|
+
Use this when the graph is still one contract, but a flat node list becomes hard to read.
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
|
|
66
|
+
- [marketing_ergonomics.rb](/Users/alex/dev/hotfix/igniter/examples/marketing_ergonomics.rb)
|
|
67
|
+
|
|
68
|
+
Use:
|
|
69
|
+
|
|
70
|
+
- routing
|
|
71
|
+
- validation
|
|
72
|
+
- pricing
|
|
73
|
+
- response shaping
|
|
74
|
+
|
|
75
|
+
Shape:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
scope :routing do
|
|
79
|
+
map :trade_name, from: :service do |service:|
|
|
80
|
+
...
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
namespace :validation do
|
|
85
|
+
guard :zip_supported, with: :zip_code, in: %w[60601 10001], message: "Unsupported zip"
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Guideline:
|
|
90
|
+
|
|
91
|
+
- use `scope` and `namespace` for readability first
|
|
92
|
+
- do not treat them as runtime boundaries
|
|
93
|
+
|
|
94
|
+
## 4. Declarative Routing
|
|
95
|
+
|
|
96
|
+
Use this when control flow depends on a selector value and should stay visible in the graph.
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
|
|
100
|
+
- [ringcentral_routing.rb](/Users/alex/dev/hotfix/igniter/examples/ringcentral_routing.rb)
|
|
101
|
+
|
|
102
|
+
Use:
|
|
103
|
+
|
|
104
|
+
- vendor routing
|
|
105
|
+
- status routing
|
|
106
|
+
- country-specific flows
|
|
107
|
+
- mode-specific processing
|
|
108
|
+
|
|
109
|
+
Shape:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
branch :status_route, with: :telephony_status, inputs: {
|
|
113
|
+
extension_id: :extension_id,
|
|
114
|
+
telephony_status: :telephony_status,
|
|
115
|
+
active_calls: :active_calls
|
|
116
|
+
} do
|
|
117
|
+
on "CallConnected", contract: CallConnectedContract
|
|
118
|
+
on "NoCall", contract: NoCallContract
|
|
119
|
+
default contract: UnknownStatusContract
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Guideline:
|
|
124
|
+
|
|
125
|
+
- keep branch contracts on a shared input interface
|
|
126
|
+
- let compile-time validation force interface consistency
|
|
127
|
+
- use explicit `export` from the branch node
|
|
128
|
+
|
|
129
|
+
## 5. Fan-Out With Stable Identity
|
|
130
|
+
|
|
131
|
+
Use this when a node should run the same child contract for many item inputs.
|
|
132
|
+
|
|
133
|
+
Examples:
|
|
134
|
+
|
|
135
|
+
- [collection.rb](/Users/alex/dev/hotfix/igniter/examples/collection.rb)
|
|
136
|
+
- [collection_partial_failure.rb](/Users/alex/dev/hotfix/igniter/examples/collection_partial_failure.rb)
|
|
137
|
+
- [ringcentral_routing.rb](/Users/alex/dev/hotfix/igniter/examples/ringcentral_routing.rb)
|
|
138
|
+
|
|
139
|
+
Use:
|
|
140
|
+
|
|
141
|
+
- technicians
|
|
142
|
+
- calls
|
|
143
|
+
- locations
|
|
144
|
+
- vendors
|
|
145
|
+
- external records
|
|
146
|
+
|
|
147
|
+
Shape:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
collection :technicians,
|
|
151
|
+
with: :technician_inputs,
|
|
152
|
+
each: TechnicianContract,
|
|
153
|
+
key: :technician_id,
|
|
154
|
+
mode: :collect
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Guideline:
|
|
158
|
+
|
|
159
|
+
- feed `collection` an array of item input hashes
|
|
160
|
+
- choose a stable `key:`
|
|
161
|
+
- keep item logic in the child contract, not in a giant parent `compute`
|
|
162
|
+
|
|
163
|
+
## 6. Partial Failure Without Failing The Whole Execution
|
|
164
|
+
|
|
165
|
+
Use `mode: :collect` when you want item-level failures surfaced, but not promoted to parent execution failure.
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
|
|
169
|
+
- [collection_partial_failure.rb](/Users/alex/dev/hotfix/igniter/examples/collection_partial_failure.rb)
|
|
170
|
+
|
|
171
|
+
What to read:
|
|
172
|
+
|
|
173
|
+
- `result.summary`
|
|
174
|
+
- `result.items_summary`
|
|
175
|
+
- `result.failed_items`
|
|
176
|
+
- `contract.diagnostics_text`
|
|
177
|
+
- `contract.diagnostics_markdown`
|
|
178
|
+
|
|
179
|
+
Guideline:
|
|
180
|
+
|
|
181
|
+
- parent execution can still be `succeeded`
|
|
182
|
+
- collection summary may be `:partial_failure`
|
|
183
|
+
- diagnostics should be read at collection level, not only at execution status level
|
|
184
|
+
|
|
185
|
+
## 7. Nested Branch + Collection
|
|
186
|
+
|
|
187
|
+
Use this when routing chooses a stage, and that stage performs fan-out or per-item routing.
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
|
|
191
|
+
- [ringcentral_routing.rb](/Users/alex/dev/hotfix/igniter/examples/ringcentral_routing.rb)
|
|
192
|
+
|
|
193
|
+
Observed semantics:
|
|
194
|
+
|
|
195
|
+
- parent execution records top-level `branch_selected`
|
|
196
|
+
- collection item events belong to the selected child execution
|
|
197
|
+
- child diagnostics is usually the best place to inspect collection status
|
|
198
|
+
|
|
199
|
+
Guideline:
|
|
200
|
+
|
|
201
|
+
- inspect parent audit for high-level routing
|
|
202
|
+
- inspect child audit for item-level fan-out behavior
|
|
203
|
+
|
|
204
|
+
## 8. Async Resume Flow
|
|
205
|
+
|
|
206
|
+
Use this when a node cannot complete immediately and should suspend the execution.
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
|
|
210
|
+
- [async_store.rb](/Users/alex/dev/hotfix/igniter/examples/async_store.rb)
|
|
211
|
+
|
|
212
|
+
Use:
|
|
213
|
+
|
|
214
|
+
- external jobs
|
|
215
|
+
- long-running enrichment
|
|
216
|
+
- async pricing or classification
|
|
217
|
+
|
|
218
|
+
Guideline:
|
|
219
|
+
|
|
220
|
+
- model the slow step as a deferred node
|
|
221
|
+
- resume with store-backed execution restore
|
|
222
|
+
- keep downstream graph pure and resumable
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Store Adapters
|
|
2
|
+
|
|
3
|
+
Igniter ships with reference execution stores for:
|
|
4
|
+
|
|
5
|
+
- memory
|
|
6
|
+
- file
|
|
7
|
+
- ActiveRecord-style persistence
|
|
8
|
+
- Redis-style persistence
|
|
9
|
+
|
|
10
|
+
All stores implement the same minimal protocol:
|
|
11
|
+
|
|
12
|
+
- `save(snapshot)`
|
|
13
|
+
- `fetch(execution_id)`
|
|
14
|
+
- `delete(execution_id)`
|
|
15
|
+
- `exist?(execution_id)`
|
|
16
|
+
|
|
17
|
+
## Memory Store
|
|
18
|
+
|
|
19
|
+
Useful for tests and single-process flows.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
Igniter.execution_store = Igniter::Runtime::Stores::MemoryStore.new
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## File Store
|
|
26
|
+
|
|
27
|
+
Useful for local development and smoke-testing worker flows.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
Igniter.execution_store = Igniter::Runtime::Stores::FileStore.new(
|
|
31
|
+
root: Rails.root.join("tmp/igniter_executions")
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## ActiveRecord Store
|
|
36
|
+
|
|
37
|
+
Expected record shape:
|
|
38
|
+
|
|
39
|
+
- one unique `execution_id` column
|
|
40
|
+
- one text/json column for serialized snapshot payload
|
|
41
|
+
|
|
42
|
+
Example model:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class IgniterExecutionSnapshot < ApplicationRecord
|
|
46
|
+
validates :execution_id, presence: true, uniqueness: true
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Example migration:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class CreateIgniterExecutionSnapshots < ActiveRecord::Migration[7.1]
|
|
54
|
+
def change
|
|
55
|
+
create_table :igniter_execution_snapshots do |t|
|
|
56
|
+
t.string :execution_id, null: false
|
|
57
|
+
t.jsonb :snapshot_json, null: false, default: {}
|
|
58
|
+
t.timestamps
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
add_index :igniter_execution_snapshots, :execution_id, unique: true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Store configuration:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Igniter.execution_store = Igniter::Runtime::Stores::ActiveRecordStore.new(
|
|
70
|
+
record_class: IgniterExecutionSnapshot,
|
|
71
|
+
execution_id_column: :execution_id,
|
|
72
|
+
snapshot_column: :snapshot_json
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Redis Store
|
|
77
|
+
|
|
78
|
+
Expected client protocol:
|
|
79
|
+
|
|
80
|
+
- `set(key, value)`
|
|
81
|
+
- `get(key)`
|
|
82
|
+
- `del(key)`
|
|
83
|
+
- `exists?(key)`
|
|
84
|
+
|
|
85
|
+
Example configuration:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
redis = Redis.new(url: ENV.fetch("REDIS_URL"))
|
|
89
|
+
|
|
90
|
+
Igniter.execution_store = Igniter::Runtime::Stores::RedisStore.new(
|
|
91
|
+
redis: redis,
|
|
92
|
+
namespace: "igniter:executions"
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Worker Flow
|
|
97
|
+
|
|
98
|
+
Store-backed contracts should declare:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
class AsyncPricingContract < Igniter::Contract
|
|
102
|
+
run_with runner: :store
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Producer side:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
contract = AsyncPricingContract.new(order_total: 100)
|
|
110
|
+
deferred = contract.result.gross_total
|
|
111
|
+
|
|
112
|
+
execution_id = contract.execution.events.execution_id
|
|
113
|
+
token = deferred.token
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Worker side:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
AsyncPricingContract.resume_from_store(
|
|
120
|
+
execution_id,
|
|
121
|
+
token: token,
|
|
122
|
+
value: 150
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If the resumed execution finishes successfully, the current `StoreRunner` deletes the persisted snapshot automatically.
|
data/examples/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
These scripts are intended to be runnable entry points for new users.
|
|
4
4
|
Each one can be executed directly from the project root with `ruby examples/<name>.rb`.
|
|
5
|
+
For higher-level guidance on when to use each style, see [PATTERNS.md](/Users/alex/dev/hotfix/igniter/docs/PATTERNS.md).
|
|
5
6
|
|
|
6
7
|
## Available Scripts
|
|
7
8
|
|
|
@@ -72,6 +73,129 @@ Outputs: gross_total=120.0
|
|
|
72
73
|
{:graph=>"PriceContract", ...}
|
|
73
74
|
```
|
|
74
75
|
|
|
76
|
+
### `async_store.rb`
|
|
77
|
+
|
|
78
|
+
Run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
ruby examples/async_store.rb
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Shows:
|
|
85
|
+
|
|
86
|
+
- deferred executor output through `defer`
|
|
87
|
+
- file-backed pending execution store
|
|
88
|
+
- restore and resume flow through `resume_from_store`
|
|
89
|
+
|
|
90
|
+
Expected output shape:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
pending_token=quote-100
|
|
94
|
+
stored_execution_id=<uuid>
|
|
95
|
+
pending_status=true
|
|
96
|
+
resumed_gross_total=180.0
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `marketing_ergonomics.rb`
|
|
100
|
+
|
|
101
|
+
Run:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
ruby examples/marketing_ergonomics.rb
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Shows:
|
|
108
|
+
|
|
109
|
+
- ergonomic helpers `with`, `const`, `lookup`, `map`, matcher-style `guard`, `expose`
|
|
110
|
+
- success-side-effect shorthand via `on_success`
|
|
111
|
+
- structural grouping via `scope` and `namespace`
|
|
112
|
+
- pre-execution planning via `contract.explain_plan`
|
|
113
|
+
- a domain-style contract that stays compact without hiding the graph
|
|
114
|
+
|
|
115
|
+
Expected output shape:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
Plan MarketingQuoteContract
|
|
119
|
+
Targets: quote
|
|
120
|
+
...
|
|
121
|
+
---
|
|
122
|
+
response={:vendor_id=>"eLocal", :trade=>"HVAC", :zip_code=>"60601", :bid=>45.0}
|
|
123
|
+
outbox=[{:vendor_id=>"eLocal", :zip_code=>"60601"}]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `collection.rb`
|
|
127
|
+
|
|
128
|
+
Run:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
ruby examples/collection.rb
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Shows:
|
|
135
|
+
|
|
136
|
+
- declarative fan-out via `collection`
|
|
137
|
+
- stable item identity via `key:`
|
|
138
|
+
- `CollectionResult` output surface
|
|
139
|
+
- per-item child contract results in `:collect` mode
|
|
140
|
+
|
|
141
|
+
Expected output shape:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
keys=[1, 2]
|
|
145
|
+
items={1=>{:key=>1, :status=>:succeeded, ...}, 2=>{:key=>2, :status=>:succeeded, ...}}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `collection_partial_failure.rb`
|
|
149
|
+
|
|
150
|
+
Run:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
ruby examples/collection_partial_failure.rb
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Shows:
|
|
157
|
+
|
|
158
|
+
- `collection` in `mode: :collect`
|
|
159
|
+
- `CollectionResult#summary`
|
|
160
|
+
- `CollectionResult#items_summary`
|
|
161
|
+
- `CollectionResult#failed_items`
|
|
162
|
+
- diagnostics output for partial collection failure without failing the whole execution
|
|
163
|
+
|
|
164
|
+
Expected output shape:
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
summary={:mode=>:collect, :total=>3, :succeeded=>2, :failed=>1, :status=>:partial_failure}
|
|
168
|
+
items_summary={1=>{:status=>:succeeded}, 2=>{:status=>:failed, ...}, ...}
|
|
169
|
+
failed_items={2=>{:type=>"Igniter::ResolutionError", ...}}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `ringcentral_routing.rb`
|
|
173
|
+
|
|
174
|
+
Run:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
ruby examples/ringcentral_routing.rb
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Shows:
|
|
181
|
+
|
|
182
|
+
- top-level routing via `branch`
|
|
183
|
+
- nested fan-out via `collection`
|
|
184
|
+
- per-item nested routing via another `branch`
|
|
185
|
+
- `CollectionResult` summary on the selected child contract
|
|
186
|
+
- the practical boundary between parent diagnostics and child diagnostics
|
|
187
|
+
|
|
188
|
+
Expected output shape:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
Plan RingcentralWebhookContract
|
|
192
|
+
...
|
|
193
|
+
---
|
|
194
|
+
routing_summary={:extension_id=>62872332031, ...}
|
|
195
|
+
status_route_branch=CallConnected
|
|
196
|
+
child_collection_summary={:mode=>:collect, :total=>3, ...}
|
|
197
|
+
```
|
|
198
|
+
|
|
75
199
|
## Validation
|
|
76
200
|
|
|
77
201
|
These scripts are exercised by [example_scripts_spec.rb](/Users/alex/dev/hotfix/igniter/spec/igniter/example_scripts_spec.rb), so the documented commands and outputs stay aligned with the code.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "igniter"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class AsyncQuoteExecutor < Igniter::Executor
|
|
8
|
+
input :order_total, type: :numeric
|
|
9
|
+
|
|
10
|
+
def call(order_total:)
|
|
11
|
+
defer(token: "quote-#{order_total}", payload: { kind: "pricing_quote" })
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class AsyncPricingContract < Igniter::Contract
|
|
16
|
+
run_with runner: :store
|
|
17
|
+
|
|
18
|
+
define do
|
|
19
|
+
input :order_total, type: :numeric
|
|
20
|
+
|
|
21
|
+
compute :quote_total, depends_on: [:order_total], call: AsyncQuoteExecutor
|
|
22
|
+
|
|
23
|
+
compute :gross_total, depends_on: [:quote_total] do |quote_total:|
|
|
24
|
+
quote_total * 1.2
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
output :gross_total
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Dir.mktmpdir("igniter-example-store") do |dir|
|
|
32
|
+
original_store = Igniter.execution_store
|
|
33
|
+
Igniter.execution_store = Igniter::Runtime::Stores::FileStore.new(root: dir)
|
|
34
|
+
|
|
35
|
+
contract = AsyncPricingContract.new(order_total: 100)
|
|
36
|
+
deferred = contract.result.gross_total
|
|
37
|
+
execution_id = contract.execution.events.execution_id
|
|
38
|
+
|
|
39
|
+
puts "pending_token=#{deferred.token}"
|
|
40
|
+
puts "stored_execution_id=#{execution_id}"
|
|
41
|
+
puts "pending_status=#{contract.result.pending?}"
|
|
42
|
+
|
|
43
|
+
resumed = AsyncPricingContract.resume_from_store(execution_id, token: deferred.token, value: 150)
|
|
44
|
+
puts "resumed_gross_total=#{resumed.result.gross_total}"
|
|
45
|
+
ensure
|
|
46
|
+
Igniter.execution_store = original_store
|
|
47
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "igniter"
|
|
5
|
+
|
|
6
|
+
class TechnicianContract < Igniter::Contract
|
|
7
|
+
define do
|
|
8
|
+
input :technician_id
|
|
9
|
+
input :name
|
|
10
|
+
|
|
11
|
+
compute :summary, with: %i[technician_id name] do |technician_id:, name:|
|
|
12
|
+
{ id: technician_id, name: name }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
output :summary
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class TechnicianBatchContract < Igniter::Contract
|
|
20
|
+
define do
|
|
21
|
+
input :technician_inputs, type: :array
|
|
22
|
+
|
|
23
|
+
collection :technicians,
|
|
24
|
+
with: :technician_inputs,
|
|
25
|
+
each: TechnicianContract,
|
|
26
|
+
key: :technician_id,
|
|
27
|
+
mode: :collect
|
|
28
|
+
|
|
29
|
+
output :technicians
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
contract = TechnicianBatchContract.new(
|
|
34
|
+
technician_inputs: [
|
|
35
|
+
{ technician_id: 1, name: "Anna" },
|
|
36
|
+
{ technician_id: 2, name: "Mike" }
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
result = contract.result.technicians
|
|
41
|
+
|
|
42
|
+
puts "keys=#{result.keys.inspect}"
|
|
43
|
+
puts "items=#{result.to_h.inspect}"
|