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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +224 -1
  4. data/docs/API_V2.md +238 -1
  5. data/docs/BACKLOG.md +166 -0
  6. data/docs/BRANCHES_V1.md +213 -0
  7. data/docs/COLLECTIONS_V1.md +303 -0
  8. data/docs/EXECUTION_MODEL_V2.md +79 -0
  9. data/docs/PATTERNS.md +222 -0
  10. data/docs/STORE_ADAPTERS.md +126 -0
  11. data/examples/README.md +124 -0
  12. data/examples/async_store.rb +47 -0
  13. data/examples/collection.rb +43 -0
  14. data/examples/collection_partial_failure.rb +50 -0
  15. data/examples/marketing_ergonomics.rb +57 -0
  16. data/examples/ringcentral_routing.rb +278 -0
  17. data/lib/igniter/compiler/compiled_graph.rb +82 -0
  18. data/lib/igniter/compiler/graph_compiler.rb +12 -2
  19. data/lib/igniter/compiler/type_resolver.rb +54 -0
  20. data/lib/igniter/compiler/validation_context.rb +61 -0
  21. data/lib/igniter/compiler/validation_pipeline.rb +30 -0
  22. data/lib/igniter/compiler/validator.rb +1 -187
  23. data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
  24. data/lib/igniter/compiler/validators/dependencies_validator.rb +151 -0
  25. data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
  26. data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
  27. data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
  28. data/lib/igniter/compiler.rb +8 -0
  29. data/lib/igniter/contract.rb +136 -4
  30. data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
  31. data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
  32. data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
  33. data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
  34. data/lib/igniter/diagnostics/report.rb +84 -8
  35. data/lib/igniter/dsl/contract_builder.rb +208 -5
  36. data/lib/igniter/dsl/schema_builder.rb +73 -0
  37. data/lib/igniter/dsl.rb +1 -0
  38. data/lib/igniter/errors.rb +11 -0
  39. data/lib/igniter/events/bus.rb +5 -0
  40. data/lib/igniter/events/event.rb +29 -0
  41. data/lib/igniter/executor.rb +74 -0
  42. data/lib/igniter/executor_registry.rb +44 -0
  43. data/lib/igniter/extensions/auditing/timeline.rb +4 -0
  44. data/lib/igniter/extensions/introspection/graph_formatter.rb +29 -3
  45. data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
  46. data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
  47. data/lib/igniter/extensions/introspection.rb +1 -0
  48. data/lib/igniter/extensions/reactive/engine.rb +49 -2
  49. data/lib/igniter/extensions/reactive/reaction.rb +3 -2
  50. data/lib/igniter/model/branch_node.rb +40 -0
  51. data/lib/igniter/model/collection_node.rb +25 -0
  52. data/lib/igniter/model/composition_node.rb +2 -2
  53. data/lib/igniter/model/compute_node.rb +58 -2
  54. data/lib/igniter/model/input_node.rb +2 -2
  55. data/lib/igniter/model/output_node.rb +24 -4
  56. data/lib/igniter/model.rb +2 -0
  57. data/lib/igniter/runtime/cache.rb +64 -25
  58. data/lib/igniter/runtime/collection_result.rb +111 -0
  59. data/lib/igniter/runtime/deferred_result.rb +40 -0
  60. data/lib/igniter/runtime/execution.rb +261 -11
  61. data/lib/igniter/runtime/input_validator.rb +2 -24
  62. data/lib/igniter/runtime/invalidator.rb +1 -1
  63. data/lib/igniter/runtime/job_worker.rb +18 -0
  64. data/lib/igniter/runtime/node_state.rb +20 -0
  65. data/lib/igniter/runtime/planner.rb +126 -0
  66. data/lib/igniter/runtime/resolver.rb +269 -15
  67. data/lib/igniter/runtime/result.rb +14 -2
  68. data/lib/igniter/runtime/runner_factory.rb +20 -0
  69. data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
  70. data/lib/igniter/runtime/runners/store_runner.rb +29 -0
  71. data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
  72. data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
  73. data/lib/igniter/runtime/stores/file_store.rb +43 -0
  74. data/lib/igniter/runtime/stores/memory_store.rb +40 -0
  75. data/lib/igniter/runtime/stores/redis_store.rb +44 -0
  76. data/lib/igniter/runtime.rb +12 -0
  77. data/lib/igniter/type_system.rb +44 -0
  78. data/lib/igniter/version.rb +1 -1
  79. data/lib/igniter.rb +23 -0
  80. metadata +43 -2
@@ -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}"