igniter 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +224 -1
- data/docs/API_V2.md +296 -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 +127 -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 +269 -0
- data/lib/igniter/compiler/compiled_graph.rb +90 -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 +153 -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 +152 -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 +186 -11
- data/lib/igniter/dsl/contract_builder.rb +271 -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 +33 -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 +46 -0
- data/lib/igniter/model/collection_node.rb +31 -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 +310 -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
|
@@ -17,6 +17,7 @@ module Igniter
|
|
|
17
17
|
outputs: serialize_outputs,
|
|
18
18
|
errors: serialize_errors,
|
|
19
19
|
nodes: summarize_nodes,
|
|
20
|
+
collection_nodes: summarize_collection_nodes,
|
|
20
21
|
events: summarize_events
|
|
21
22
|
}
|
|
22
23
|
end
|
|
@@ -31,8 +32,9 @@ module Igniter
|
|
|
31
32
|
lines << "Diagnostics #{report[:graph]}"
|
|
32
33
|
lines << "Execution #{report[:execution_id]}"
|
|
33
34
|
lines << "Status: #{report[:status]}"
|
|
34
|
-
lines << format_outputs(
|
|
35
|
+
lines << format_outputs(presented_outputs)
|
|
35
36
|
lines << format_nodes(report[:nodes])
|
|
37
|
+
lines << format_collection_nodes(report[:collection_nodes])
|
|
36
38
|
lines << format_errors(report[:errors])
|
|
37
39
|
lines << format_events(report[:events])
|
|
38
40
|
lines.compact.join("\n")
|
|
@@ -45,8 +47,11 @@ module Igniter
|
|
|
45
47
|
lines << ""
|
|
46
48
|
lines << "- Execution: `#{report[:execution_id]}`"
|
|
47
49
|
lines << "- Status: `#{report[:status]}`"
|
|
48
|
-
lines << "- Outputs: #{inline_hash(
|
|
50
|
+
lines << "- Outputs: #{inline_hash(presented_outputs)}"
|
|
49
51
|
lines << "- Nodes: total=#{report[:nodes][:total]}, succeeded=#{report[:nodes][:succeeded]}, failed=#{report[:nodes][:failed]}, stale=#{report[:nodes][:stale]}"
|
|
52
|
+
unless report[:collection_nodes].empty?
|
|
53
|
+
lines << "- Collections: #{report[:collection_nodes].map { |node| "#{node[:node_name]} total=#{node[:total]} succeeded=#{node[:succeeded]} failed=#{node[:failed]} status=#{node[:status]}" }.join('; ')}"
|
|
54
|
+
end
|
|
50
55
|
lines << "- Events: total=#{report[:events][:total]}, latest=#{report[:events][:latest_type] || 'none'}"
|
|
51
56
|
|
|
52
57
|
unless report[:errors].empty?
|
|
@@ -57,6 +62,17 @@ module Igniter
|
|
|
57
62
|
end
|
|
58
63
|
end
|
|
59
64
|
|
|
65
|
+
unless report[:collection_nodes].empty?
|
|
66
|
+
lines << ""
|
|
67
|
+
lines << "## Collections"
|
|
68
|
+
report[:collection_nodes].each do |node|
|
|
69
|
+
lines << "- `#{node[:node_name]}`: total=#{node[:total]}, succeeded=#{node[:succeeded]}, failed=#{node[:failed]}, status=#{node[:status]}"
|
|
70
|
+
node[:failed_items].each do |item|
|
|
71
|
+
lines << "- `#{node[:node_name]}[#{item[:key]}]` failed: #{item[:message]}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
60
76
|
lines.join("\n")
|
|
61
77
|
end
|
|
62
78
|
|
|
@@ -76,36 +92,52 @@ module Igniter
|
|
|
76
92
|
|
|
77
93
|
def status
|
|
78
94
|
return :failed if execution.cache.values.any?(&:failed?)
|
|
95
|
+
return :pending if execution.cache.values.any?(&:pending?)
|
|
79
96
|
return :stale if execution.cache.values.any?(&:stale?)
|
|
80
97
|
|
|
81
98
|
:succeeded
|
|
82
99
|
end
|
|
83
100
|
|
|
84
101
|
def serialize_errors
|
|
85
|
-
|
|
102
|
+
execution.cache.values.filter_map do |state|
|
|
103
|
+
next unless state.failed?
|
|
104
|
+
|
|
86
105
|
{
|
|
87
|
-
node_name:
|
|
88
|
-
type: error.class.name,
|
|
89
|
-
message: error.message,
|
|
90
|
-
context: error.respond_to?(:context) ? error.context : {}
|
|
106
|
+
node_name: state.node.name,
|
|
107
|
+
type: state.error.class.name,
|
|
108
|
+
message: state.error.message,
|
|
109
|
+
context: state.error.respond_to?(:context) ? state.error.context : {}
|
|
91
110
|
}
|
|
92
111
|
end
|
|
93
112
|
end
|
|
94
113
|
|
|
95
114
|
def serialize_outputs
|
|
96
115
|
execution.compiled_graph.outputs.each_with_object({}) do |output_node, memo|
|
|
97
|
-
state = execution.cache.fetch(output_node.
|
|
98
|
-
memo[output_node.name] =
|
|
116
|
+
state = execution.cache.fetch(output_node.source_root)
|
|
117
|
+
memo[output_node.name] = serialize_output_value(output_node, state)
|
|
99
118
|
end
|
|
100
119
|
end
|
|
101
120
|
|
|
102
|
-
def
|
|
121
|
+
def serialize_output_value(output_node, state)
|
|
103
122
|
return nil unless state
|
|
104
123
|
return { error: state.error.message, status: state.status } if state.failed?
|
|
105
124
|
|
|
125
|
+
if output_node.composition_output?
|
|
126
|
+
return serialize_output_from_child(output_node, state.value)
|
|
127
|
+
end
|
|
128
|
+
|
|
106
129
|
serialize_value(state.value)
|
|
107
130
|
end
|
|
108
131
|
|
|
132
|
+
def serialize_output_from_child(output_node, child_result)
|
|
133
|
+
return nil unless child_result.is_a?(Runtime::Result)
|
|
134
|
+
|
|
135
|
+
child_errors = child_result.execution.cache.values.select(&:failed?)
|
|
136
|
+
return { error: child_errors.first.error.message, status: :failed } unless child_errors.empty?
|
|
137
|
+
|
|
138
|
+
child_result.public_send(output_node.child_output_name)
|
|
139
|
+
end
|
|
140
|
+
|
|
109
141
|
def summarize_nodes
|
|
110
142
|
states = execution.states
|
|
111
143
|
|
|
@@ -113,6 +145,7 @@ module Igniter
|
|
|
113
145
|
total: states.size,
|
|
114
146
|
succeeded: states.values.count { |state| state[:status] == :succeeded },
|
|
115
147
|
failed: states.values.count { |state| state[:status] == :failed },
|
|
148
|
+
pending: states.values.count { |state| state[:status] == :pending },
|
|
116
149
|
stale: states.values.count { |state| state[:status] == :stale },
|
|
117
150
|
failed_nodes: states.filter_map do |node_name, state|
|
|
118
151
|
next unless state[:status] == :failed
|
|
@@ -137,8 +170,16 @@ module Igniter
|
|
|
137
170
|
"Outputs: #{inline_hash(outputs)}"
|
|
138
171
|
end
|
|
139
172
|
|
|
173
|
+
def presented_outputs
|
|
174
|
+
@presented_outputs ||= execution.compiled_graph.outputs.each_with_object({}) do |output_node, memo|
|
|
175
|
+
raw_value = to_h[:outputs][output_node.name]
|
|
176
|
+
memo[output_node.name] = present_output(output_node.name, raw_value)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
140
180
|
def format_nodes(nodes)
|
|
141
181
|
line = "Nodes: total=#{nodes[:total]}, succeeded=#{nodes[:succeeded]}, failed=#{nodes[:failed]}, stale=#{nodes[:stale]}"
|
|
182
|
+
line = "Nodes: total=#{nodes[:total]}, succeeded=#{nodes[:succeeded]}, failed=#{nodes[:failed]}, pending=#{nodes[:pending]}, stale=#{nodes[:stale]}"
|
|
142
183
|
return line if nodes[:failed_nodes].empty?
|
|
143
184
|
|
|
144
185
|
failures = nodes[:failed_nodes].map { |node| "#{node[:node_name]}(#{node[:error]})" }.join(", ")
|
|
@@ -151,24 +192,158 @@ module Igniter
|
|
|
151
192
|
"Errors: #{errors.map { |error| "#{error[:node_name]}=#{error[:type]}" }.join(', ')}"
|
|
152
193
|
end
|
|
153
194
|
|
|
195
|
+
def format_collection_nodes(collection_nodes)
|
|
196
|
+
return nil if collection_nodes.empty?
|
|
197
|
+
|
|
198
|
+
summaries = collection_nodes.map do |node|
|
|
199
|
+
summary = "#{node[:node_name]} total=#{node[:total]} succeeded=#{node[:succeeded]} failed=#{node[:failed]} status=#{node[:status]}"
|
|
200
|
+
next summary if node[:failed_items].empty?
|
|
201
|
+
|
|
202
|
+
"#{summary} failed_items=#{node[:failed_items].map { |item| "#{item[:key]}(#{item[:message]})" }.join(', ')}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
"Collections: #{summaries.join('; ')}"
|
|
206
|
+
end
|
|
207
|
+
|
|
154
208
|
def format_events(events)
|
|
155
209
|
"Events: total=#{events[:total]}, latest=#{events[:latest_type] || 'none'}"
|
|
156
210
|
end
|
|
157
211
|
|
|
158
212
|
def inline_hash(hash)
|
|
159
|
-
hash.map { |key, value| "#{key}=#{value
|
|
213
|
+
hash.map { |key, value| "#{key}=#{inline_value(value)}" }.join(", ")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def present_output(output_name, raw_value)
|
|
217
|
+
presenter = execution.contract_instance.class.output_presenters[output_name.to_sym]
|
|
218
|
+
return raw_value unless presenter
|
|
219
|
+
|
|
220
|
+
if presenter.is_a?(Symbol) || presenter.is_a?(String)
|
|
221
|
+
execution.contract_instance.public_send(
|
|
222
|
+
presenter,
|
|
223
|
+
value: raw_value,
|
|
224
|
+
contract: execution.contract_instance,
|
|
225
|
+
execution: execution
|
|
226
|
+
)
|
|
227
|
+
else
|
|
228
|
+
presenter.call(
|
|
229
|
+
value: raw_value,
|
|
230
|
+
contract: execution.contract_instance,
|
|
231
|
+
execution: execution
|
|
232
|
+
)
|
|
233
|
+
end
|
|
160
234
|
end
|
|
161
235
|
|
|
162
236
|
def serialize_value(value)
|
|
163
237
|
case value
|
|
238
|
+
when Runtime::DeferredResult
|
|
239
|
+
value.as_json
|
|
164
240
|
when Runtime::Result
|
|
165
241
|
value.to_h
|
|
242
|
+
when Runtime::CollectionResult
|
|
243
|
+
value.as_json
|
|
166
244
|
when Array
|
|
167
245
|
value.map { |item| serialize_value(item) }
|
|
168
246
|
else
|
|
169
247
|
value
|
|
170
248
|
end
|
|
171
249
|
end
|
|
250
|
+
|
|
251
|
+
def inline_value(value)
|
|
252
|
+
case value
|
|
253
|
+
when Hash
|
|
254
|
+
return summarize_serialized_collection_hash(value) if serialized_collection_hash?(value)
|
|
255
|
+
return summarize_serialized_collection_items_hash(value) if serialized_collection_items_hash?(value)
|
|
256
|
+
|
|
257
|
+
"{#{value.map { |key, nested| "#{key}: #{inline_value(nested)}" }.join(', ')}}"
|
|
258
|
+
when Array
|
|
259
|
+
"[#{value.map { |item| inline_value(item) }.join(', ')}]"
|
|
260
|
+
when Runtime::Result
|
|
261
|
+
summarize_nested_result(value)
|
|
262
|
+
when Runtime::CollectionResult
|
|
263
|
+
summarize_collection_result(value)
|
|
264
|
+
else
|
|
265
|
+
value.inspect
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def summarize_nested_result(result)
|
|
270
|
+
outputs = result.to_h.keys
|
|
271
|
+
"{graph=#{result.execution.compiled_graph.name.inspect}, status=#{nested_result_status(result).inspect}, outputs=#{outputs.inspect}}"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def summarize_collection_result(result)
|
|
275
|
+
summary = result.summary
|
|
276
|
+
failed_keys = result.failures.keys
|
|
277
|
+
"{mode=#{result.mode.inspect}, total=#{summary[:total]}, succeeded=#{summary[:succeeded]}, failed=#{summary[:failed]}, status=#{summary[:status].inspect}, keys=#{result.keys.inspect}, failed_keys=#{failed_keys.inspect}}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def serialized_collection_hash?(value)
|
|
281
|
+
value.key?(:mode) && value.key?(:summary) && value.key?(:items)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def summarize_serialized_collection_hash(value)
|
|
285
|
+
summary = value[:summary] || {}
|
|
286
|
+
items = value[:items] || {}
|
|
287
|
+
failed_keys = items.each_with_object([]) do |(key, item), memo|
|
|
288
|
+
memo << key if item[:status] == :failed || item["status"] == :failed
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
"{mode=#{value[:mode].inspect}, total=#{summary[:total]}, succeeded=#{summary[:succeeded]}, failed=#{summary[:failed]}, status=#{summary[:status].inspect}, keys=#{items.keys.inspect}, failed_keys=#{failed_keys.inspect}}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def serialized_collection_items_hash?(value)
|
|
295
|
+
return false if value.empty?
|
|
296
|
+
|
|
297
|
+
value.values.all? do |item|
|
|
298
|
+
item.is_a?(Hash) && (item.key?(:key) || item.key?("key")) && (item.key?(:status) || item.key?("status"))
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def summarize_serialized_collection_items_hash(value)
|
|
303
|
+
failed_keys = value.each_with_object([]) do |(key, item), memo|
|
|
304
|
+
status = item[:status] || item["status"]
|
|
305
|
+
memo << key if status == :failed || status == "failed"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
total = value.size
|
|
309
|
+
failed = failed_keys.size
|
|
310
|
+
succeeded = total - failed
|
|
311
|
+
status = failed.zero? ? :succeeded : :partial_failure
|
|
312
|
+
|
|
313
|
+
"{mode=:collect, total=#{total}, succeeded=#{succeeded}, failed=#{failed}, status=#{status.inspect}, keys=#{value.keys.inspect}, failed_keys=#{failed_keys.inspect}}"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def nested_result_status(result)
|
|
317
|
+
return :failed if result.failed?
|
|
318
|
+
return :pending if result.pending?
|
|
319
|
+
|
|
320
|
+
:succeeded
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def summarize_collection_nodes
|
|
324
|
+
execution.cache.values.filter_map do |state|
|
|
325
|
+
next unless state.value.is_a?(Runtime::CollectionResult)
|
|
326
|
+
|
|
327
|
+
result = state.value
|
|
328
|
+
{
|
|
329
|
+
node_name: state.node.name,
|
|
330
|
+
path: state.node.path,
|
|
331
|
+
mode: result.mode,
|
|
332
|
+
total: result.items.size,
|
|
333
|
+
succeeded: result.successes.size,
|
|
334
|
+
failed: result.failures.size,
|
|
335
|
+
status: result.failures.empty? ? :succeeded : :partial_failure,
|
|
336
|
+
failed_items: result.failures.values.map do |item|
|
|
337
|
+
{
|
|
338
|
+
key: item.key,
|
|
339
|
+
type: item.error.class.name,
|
|
340
|
+
message: item.error.message,
|
|
341
|
+
context: item.error.respond_to?(:context) ? item.error.context : {}
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
end
|
|
172
347
|
end
|
|
173
348
|
end
|
|
174
349
|
end
|
|
@@ -11,9 +11,13 @@ module Igniter
|
|
|
11
11
|
@name = name
|
|
12
12
|
@nodes = []
|
|
13
13
|
@sequence = 0
|
|
14
|
+
@scope_stack = []
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
UNDEFINED_INPUT_DEFAULT = :__igniter_undefined__
|
|
18
|
+
UNDEFINED_CONST_VALUE = :__igniter_const_undefined__
|
|
19
|
+
UNDEFINED_GUARD_MATCHER = :__igniter_guard_matcher_undefined__
|
|
20
|
+
UNDEFINED_PROJECT_OPTION = :__igniter_project_undefined__
|
|
17
21
|
|
|
18
22
|
def input(name, type: nil, required: nil, default: UNDEFINED_INPUT_DEFAULT, **metadata)
|
|
19
23
|
input_metadata = with_source_location(metadata)
|
|
@@ -25,33 +29,141 @@ module Igniter
|
|
|
25
29
|
Model::InputNode.new(
|
|
26
30
|
id: next_id,
|
|
27
31
|
name: name,
|
|
32
|
+
path: scoped_path(name),
|
|
28
33
|
metadata: input_metadata
|
|
29
34
|
)
|
|
30
35
|
)
|
|
31
36
|
end
|
|
32
37
|
|
|
33
|
-
def compute(name, depends_on
|
|
34
|
-
callable = call
|
|
38
|
+
def compute(name, depends_on: nil, with: nil, call: nil, executor: nil, **metadata, &block)
|
|
39
|
+
callable, resolved_metadata = resolve_compute_callable(call: call, executor: executor, metadata: metadata, block: block)
|
|
35
40
|
raise CompileError, "compute :#{name} requires a callable" unless callable
|
|
36
|
-
raise CompileError, "compute :#{name} cannot accept both `call:` and a block" if call && block
|
|
37
41
|
|
|
38
42
|
add_node(
|
|
39
43
|
Model::ComputeNode.new(
|
|
40
44
|
id: next_id,
|
|
41
45
|
name: name,
|
|
42
|
-
dependencies:
|
|
46
|
+
dependencies: normalize_dependencies(depends_on: depends_on, with: with),
|
|
43
47
|
callable: callable,
|
|
44
|
-
|
|
48
|
+
path: scoped_path(name),
|
|
49
|
+
metadata: with_source_location(resolved_metadata)
|
|
45
50
|
)
|
|
46
51
|
)
|
|
47
52
|
end
|
|
48
53
|
|
|
54
|
+
def const(name, value = UNDEFINED_CONST_VALUE, **metadata, &block)
|
|
55
|
+
raise CompileError, "const :#{name} cannot accept both a value and a block" if !block.nil? && value != UNDEFINED_CONST_VALUE
|
|
56
|
+
raise CompileError, "const :#{name} requires a value or a block" if block.nil? && value == UNDEFINED_CONST_VALUE
|
|
57
|
+
|
|
58
|
+
callable = if block
|
|
59
|
+
block
|
|
60
|
+
else
|
|
61
|
+
proc { value }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
compute(name, with: [], call: callable, **metadata.merge(kind: :const))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def lookup(name, depends_on: nil, with: nil, call: nil, executor: nil, **metadata, &block)
|
|
68
|
+
compute(name, depends_on: depends_on, with: with, call: call, executor: executor, **{ category: :lookup }.merge(metadata), &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def map(name, from:, call: nil, executor: nil, **metadata, &block)
|
|
72
|
+
compute(name, with: from, call: call, executor: executor, **{ category: :map }.merge(metadata), &block)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def project(name, from:, key: UNDEFINED_PROJECT_OPTION, dig: UNDEFINED_PROJECT_OPTION, default: UNDEFINED_PROJECT_OPTION, **metadata)
|
|
76
|
+
if key != UNDEFINED_PROJECT_OPTION && dig != UNDEFINED_PROJECT_OPTION
|
|
77
|
+
raise CompileError, "project :#{name} cannot use both `key:` and `dig:`"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if key == UNDEFINED_PROJECT_OPTION && dig == UNDEFINED_PROJECT_OPTION
|
|
81
|
+
raise CompileError, "project :#{name} requires either `key:` or `dig:`"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
callable = proc do |**values|
|
|
85
|
+
source = values.fetch(from.to_sym)
|
|
86
|
+
extract_projected_value(
|
|
87
|
+
source,
|
|
88
|
+
key: key,
|
|
89
|
+
dig: dig,
|
|
90
|
+
default: default,
|
|
91
|
+
node_name: name
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
compute(name, with: from, call: callable, **{ category: :project }.merge(metadata))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def aggregate(name, depends_on: nil, with: nil, call: nil, executor: nil, **metadata, &block)
|
|
99
|
+
compute(name, depends_on: depends_on, with: with, call: call, executor: executor, **{ category: :aggregate }.merge(metadata), &block)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def guard(name, depends_on: nil, with: nil, call: nil, executor: nil, message: nil,
|
|
103
|
+
eq: UNDEFINED_GUARD_MATCHER, in: UNDEFINED_GUARD_MATCHER, matches: UNDEFINED_GUARD_MATCHER,
|
|
104
|
+
**metadata, &block)
|
|
105
|
+
matcher_options = {
|
|
106
|
+
eq: eq,
|
|
107
|
+
in: binding.local_variable_get(:in),
|
|
108
|
+
matches: matches
|
|
109
|
+
}.reject { |_key, value| value == UNDEFINED_GUARD_MATCHER }
|
|
110
|
+
|
|
111
|
+
if matcher_options.any?
|
|
112
|
+
raise CompileError, "guard :#{name} cannot combine matcher options with `call:`, `executor:`, or a block" if call || executor || block
|
|
113
|
+
raise CompileError, "guard :#{name} supports only one matcher option at a time" if matcher_options.size > 1
|
|
114
|
+
|
|
115
|
+
dependencies = normalize_dependencies(depends_on: depends_on, with: with)
|
|
116
|
+
raise CompileError, "guard :#{name} with matcher options requires exactly one dependency" unless dependencies.size == 1
|
|
117
|
+
|
|
118
|
+
dependency = dependencies.first
|
|
119
|
+
matcher_name, matcher_value = matcher_options.first
|
|
120
|
+
|
|
121
|
+
call = build_guard_matcher(matcher_name, matcher_value, dependency)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
compute(
|
|
125
|
+
name,
|
|
126
|
+
depends_on: depends_on,
|
|
127
|
+
with: with,
|
|
128
|
+
call: call,
|
|
129
|
+
executor: executor,
|
|
130
|
+
**metadata.merge(kind: :guard, guard: true, guard_message: message || "Guard '#{name}' failed"),
|
|
131
|
+
&block
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def export(*names, from:, **metadata)
|
|
136
|
+
names.each do |name|
|
|
137
|
+
output(name, from: "#{from}.#{name}", **metadata)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def expose(*sources, as: nil, **metadata)
|
|
142
|
+
raise CompileError, "expose cannot use `as:` with multiple sources" if as && sources.size != 1
|
|
143
|
+
|
|
144
|
+
sources.each do |source|
|
|
145
|
+
output(as || source, from: source, **metadata)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def scope(name, &block)
|
|
150
|
+
raise CompileError, "scope requires a block" unless block
|
|
151
|
+
|
|
152
|
+
@scope_stack << name.to_s
|
|
153
|
+
instance_eval(&block)
|
|
154
|
+
ensure
|
|
155
|
+
@scope_stack.pop
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
alias namespace scope
|
|
159
|
+
|
|
49
160
|
def output(name, from: nil, **metadata)
|
|
50
161
|
add_node(
|
|
51
162
|
Model::OutputNode.new(
|
|
52
163
|
id: next_id,
|
|
53
164
|
name: name,
|
|
54
165
|
source: (from || name),
|
|
166
|
+
path: scoped_output_path(name),
|
|
55
167
|
metadata: with_source_location(metadata)
|
|
56
168
|
)
|
|
57
169
|
)
|
|
@@ -66,6 +178,51 @@ module Igniter
|
|
|
66
178
|
name: name,
|
|
67
179
|
contract_class: contract,
|
|
68
180
|
input_mapping: inputs,
|
|
181
|
+
path: scoped_path(name),
|
|
182
|
+
metadata: with_source_location(metadata)
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def branch(name, with:, inputs: nil, depends_on: nil, map_inputs: nil, using: nil, **metadata, &block)
|
|
188
|
+
raise CompileError, "branch :#{name} requires a block" unless block
|
|
189
|
+
raise CompileError, "branch :#{name} requires either `inputs:` or `map_inputs:`/`using:`" if inputs.nil? && map_inputs.nil? && using.nil?
|
|
190
|
+
raise CompileError, "branch :#{name} cannot combine `inputs:` with `map_inputs:` or `using:`" if inputs && (map_inputs || using)
|
|
191
|
+
raise CompileError, "branch :#{name} cannot use both `map_inputs:` and `using:`" if map_inputs && using
|
|
192
|
+
raise CompileError, "branch :#{name} requires an `inputs:` hash" if inputs && !inputs.is_a?(Hash)
|
|
193
|
+
|
|
194
|
+
definition = BranchBuilder.build(&block)
|
|
195
|
+
|
|
196
|
+
add_node(
|
|
197
|
+
Model::BranchNode.new(
|
|
198
|
+
id: next_id,
|
|
199
|
+
name: name,
|
|
200
|
+
selector_dependency: with,
|
|
201
|
+
cases: definition[:cases],
|
|
202
|
+
default_contract: definition[:default_contract],
|
|
203
|
+
input_mapping: inputs || {},
|
|
204
|
+
context_dependencies: normalize_dependencies(depends_on: depends_on, with: nil),
|
|
205
|
+
input_mapper: map_inputs || using,
|
|
206
|
+
path: scoped_path(name),
|
|
207
|
+
metadata: with_source_location(metadata)
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def collection(name, with:, each:, key:, mode: :collect, depends_on: nil, map_inputs: nil, using: nil, **metadata)
|
|
213
|
+
raise CompileError, "collection :#{name} cannot use both `map_inputs:` and `using:`" if map_inputs && using
|
|
214
|
+
|
|
215
|
+
add_node(
|
|
216
|
+
Model::CollectionNode.new(
|
|
217
|
+
id: next_id,
|
|
218
|
+
name: name,
|
|
219
|
+
source_dependency: with,
|
|
220
|
+
contract_class: each,
|
|
221
|
+
key_name: key,
|
|
222
|
+
mode: mode,
|
|
223
|
+
context_dependencies: normalize_dependencies(depends_on: depends_on, with: nil),
|
|
224
|
+
input_mapper: map_inputs || using,
|
|
225
|
+
path: scoped_path(name),
|
|
69
226
|
metadata: with_source_location(metadata)
|
|
70
227
|
)
|
|
71
228
|
)
|
|
@@ -90,6 +247,115 @@ module Igniter
|
|
|
90
247
|
def with_source_location(metadata)
|
|
91
248
|
metadata.merge(source_location: caller_locations(2, 1).first&.to_s)
|
|
92
249
|
end
|
|
250
|
+
|
|
251
|
+
def resolve_compute_callable(call:, executor:, metadata:, block:)
|
|
252
|
+
raise CompileError, "compute cannot accept both `call:` and `executor:`" if call && executor
|
|
253
|
+
raise CompileError, "compute cannot accept both `call:` and a block" if call && block
|
|
254
|
+
raise CompileError, "compute cannot accept both `executor:` and a block" if executor && block
|
|
255
|
+
|
|
256
|
+
if executor
|
|
257
|
+
definition = Igniter.executor_registry.fetch(executor)
|
|
258
|
+
return [definition.executor_class, definition.metadata.merge(metadata).merge(executor_key: definition.key)]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
[call || block, metadata]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def normalize_dependencies(depends_on:, with:)
|
|
265
|
+
raise CompileError, "Use either `depends_on:` or `with:`, not both" if depends_on && with
|
|
266
|
+
|
|
267
|
+
dependencies = depends_on || with
|
|
268
|
+
Array(dependencies)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def build_guard_matcher(matcher_name, matcher_value, dependency)
|
|
272
|
+
case matcher_name
|
|
273
|
+
when :eq
|
|
274
|
+
proc do |**values|
|
|
275
|
+
values.fetch(dependency) == matcher_value
|
|
276
|
+
end
|
|
277
|
+
when :in
|
|
278
|
+
allowed_values = Array(matcher_value)
|
|
279
|
+
proc do |**values|
|
|
280
|
+
allowed_values.include?(values.fetch(dependency))
|
|
281
|
+
end
|
|
282
|
+
when :matches
|
|
283
|
+
matcher = matcher_value
|
|
284
|
+
raise CompileError, "`matches:` expects a Regexp" unless matcher.is_a?(Regexp)
|
|
285
|
+
|
|
286
|
+
proc do |**values|
|
|
287
|
+
values.fetch(dependency).to_s.match?(matcher)
|
|
288
|
+
end
|
|
289
|
+
else
|
|
290
|
+
raise CompileError, "Unsupported guard matcher: #{matcher_name}"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def extract_projected_value(source, key:, dig:, default:, node_name:)
|
|
295
|
+
if key != UNDEFINED_PROJECT_OPTION
|
|
296
|
+
return fetch_project_value(source, key, default, node_name)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
current = source
|
|
300
|
+
Array(dig).each do |part|
|
|
301
|
+
current = fetch_project_value(current, part, default, node_name)
|
|
302
|
+
end
|
|
303
|
+
current
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def fetch_project_value(source, part, default, node_name)
|
|
307
|
+
if source.is_a?(Hash)
|
|
308
|
+
return source.fetch(part) if source.key?(part)
|
|
309
|
+
return source.fetch(part.to_s) if source.key?(part.to_s)
|
|
310
|
+
return source.fetch(part.to_sym) if source.key?(part.to_sym)
|
|
311
|
+
elsif source.respond_to?(part)
|
|
312
|
+
return source.public_send(part)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
return default unless default == UNDEFINED_PROJECT_OPTION
|
|
316
|
+
|
|
317
|
+
raise ResolutionError, "project :#{node_name} could not extract #{part.inspect}"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def scoped_path(name)
|
|
321
|
+
return name.to_s if @scope_stack.empty?
|
|
322
|
+
|
|
323
|
+
"#{@scope_stack.join('.')}.#{name}"
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def scoped_output_path(name)
|
|
327
|
+
return "output.#{name}" if @scope_stack.empty?
|
|
328
|
+
|
|
329
|
+
"#{@scope_stack.join('.')}.output.#{name}"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
class BranchBuilder
|
|
333
|
+
def self.build(&block)
|
|
334
|
+
new.tap { |builder| builder.instance_eval(&block) }.to_h
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def initialize
|
|
338
|
+
@cases = []
|
|
339
|
+
@default_contract = nil
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def on(match, contract:)
|
|
343
|
+
@cases << { match: match, contract: contract }
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def default(contract:)
|
|
347
|
+
raise CompileError, "branch can define only one `default`" if @default_contract
|
|
348
|
+
|
|
349
|
+
@default_contract = contract
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def to_h
|
|
353
|
+
{
|
|
354
|
+
cases: @cases,
|
|
355
|
+
default_contract: @default_contract
|
|
356
|
+
}
|
|
357
|
+
end
|
|
358
|
+
end
|
|
93
359
|
end
|
|
94
360
|
end
|
|
95
361
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module DSL
|
|
5
|
+
class SchemaBuilder
|
|
6
|
+
def self.compile(schema, name: nil)
|
|
7
|
+
new(schema, name: name).compile
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(schema, name: nil)
|
|
11
|
+
@schema = symbolize(schema)
|
|
12
|
+
@name = name || @schema[:name] || "AnonymousContract"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def compile
|
|
16
|
+
schema = @schema
|
|
17
|
+
|
|
18
|
+
ContractBuilder.compile(name: @name) do
|
|
19
|
+
Array(schema[:inputs]).each do |input_config|
|
|
20
|
+
config = input_config
|
|
21
|
+
input(
|
|
22
|
+
config.fetch(:name),
|
|
23
|
+
type: config[:type],
|
|
24
|
+
required: config[:required],
|
|
25
|
+
default: config.fetch(:default, ContractBuilder::UNDEFINED_INPUT_DEFAULT),
|
|
26
|
+
**config.fetch(:metadata, {})
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Array(schema[:compositions]).each do |composition_config|
|
|
31
|
+
config = composition_config
|
|
32
|
+
compose(
|
|
33
|
+
config.fetch(:name),
|
|
34
|
+
contract: config.fetch(:contract),
|
|
35
|
+
inputs: config.fetch(:inputs),
|
|
36
|
+
**config.fetch(:metadata, {})
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Array(schema[:computes]).each do |compute_config|
|
|
41
|
+
config = compute_config
|
|
42
|
+
options = {
|
|
43
|
+
depends_on: Array(config.fetch(:depends_on)).map(&:to_sym)
|
|
44
|
+
}
|
|
45
|
+
options[:call] = config[:call] if config.key?(:call)
|
|
46
|
+
options[:executor] = config[:executor] if config.key?(:executor)
|
|
47
|
+
compute(config.fetch(:name), **options, **config.fetch(:metadata, {}))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Array(schema[:outputs]).each do |output_config|
|
|
51
|
+
config = output_config
|
|
52
|
+
output(config.fetch(:name), from: config[:from], **config.fetch(:metadata, {}))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def symbolize(value)
|
|
60
|
+
case value
|
|
61
|
+
when Hash
|
|
62
|
+
value.each_with_object({}) do |(key, nested), memo|
|
|
63
|
+
memo[key.to_sym] = symbolize(nested)
|
|
64
|
+
end
|
|
65
|
+
when Array
|
|
66
|
+
value.map { |item| symbolize(item) }
|
|
67
|
+
else
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|