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
|
@@ -11,9 +11,12 @@ 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__
|
|
17
20
|
|
|
18
21
|
def input(name, type: nil, required: nil, default: UNDEFINED_INPUT_DEFAULT, **metadata)
|
|
19
22
|
input_metadata = with_source_location(metadata)
|
|
@@ -25,33 +28,114 @@ module Igniter
|
|
|
25
28
|
Model::InputNode.new(
|
|
26
29
|
id: next_id,
|
|
27
30
|
name: name,
|
|
31
|
+
path: scoped_path(name),
|
|
28
32
|
metadata: input_metadata
|
|
29
33
|
)
|
|
30
34
|
)
|
|
31
35
|
end
|
|
32
36
|
|
|
33
|
-
def compute(name, depends_on
|
|
34
|
-
callable = call
|
|
37
|
+
def compute(name, depends_on: nil, with: nil, call: nil, executor: nil, **metadata, &block)
|
|
38
|
+
callable, resolved_metadata = resolve_compute_callable(call: call, executor: executor, metadata: metadata, block: block)
|
|
35
39
|
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
40
|
|
|
38
41
|
add_node(
|
|
39
42
|
Model::ComputeNode.new(
|
|
40
43
|
id: next_id,
|
|
41
44
|
name: name,
|
|
42
|
-
dependencies:
|
|
45
|
+
dependencies: normalize_dependencies(depends_on: depends_on, with: with),
|
|
43
46
|
callable: callable,
|
|
44
|
-
|
|
47
|
+
path: scoped_path(name),
|
|
48
|
+
metadata: with_source_location(resolved_metadata)
|
|
45
49
|
)
|
|
46
50
|
)
|
|
47
51
|
end
|
|
48
52
|
|
|
53
|
+
def const(name, value = UNDEFINED_CONST_VALUE, **metadata, &block)
|
|
54
|
+
raise CompileError, "const :#{name} cannot accept both a value and a block" if !block.nil? && value != UNDEFINED_CONST_VALUE
|
|
55
|
+
raise CompileError, "const :#{name} requires a value or a block" if block.nil? && value == UNDEFINED_CONST_VALUE
|
|
56
|
+
|
|
57
|
+
callable = if block
|
|
58
|
+
block
|
|
59
|
+
else
|
|
60
|
+
proc { value }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
compute(name, with: [], call: callable, **metadata.merge(kind: :const))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def lookup(name, depends_on: nil, with: nil, call: nil, executor: nil, **metadata, &block)
|
|
67
|
+
compute(name, depends_on: depends_on, with: with, call: call, executor: executor, **{ category: :lookup }.merge(metadata), &block)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def map(name, from:, call: nil, executor: nil, **metadata, &block)
|
|
71
|
+
compute(name, with: from, call: call, executor: executor, **{ category: :map }.merge(metadata), &block)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def guard(name, depends_on: nil, with: nil, call: nil, executor: nil, message: nil,
|
|
75
|
+
eq: UNDEFINED_GUARD_MATCHER, in: UNDEFINED_GUARD_MATCHER, matches: UNDEFINED_GUARD_MATCHER,
|
|
76
|
+
**metadata, &block)
|
|
77
|
+
matcher_options = {
|
|
78
|
+
eq: eq,
|
|
79
|
+
in: binding.local_variable_get(:in),
|
|
80
|
+
matches: matches
|
|
81
|
+
}.reject { |_key, value| value == UNDEFINED_GUARD_MATCHER }
|
|
82
|
+
|
|
83
|
+
if matcher_options.any?
|
|
84
|
+
raise CompileError, "guard :#{name} cannot combine matcher options with `call:`, `executor:`, or a block" if call || executor || block
|
|
85
|
+
raise CompileError, "guard :#{name} supports only one matcher option at a time" if matcher_options.size > 1
|
|
86
|
+
|
|
87
|
+
dependencies = normalize_dependencies(depends_on: depends_on, with: with)
|
|
88
|
+
raise CompileError, "guard :#{name} with matcher options requires exactly one dependency" unless dependencies.size == 1
|
|
89
|
+
|
|
90
|
+
dependency = dependencies.first
|
|
91
|
+
matcher_name, matcher_value = matcher_options.first
|
|
92
|
+
|
|
93
|
+
call = build_guard_matcher(matcher_name, matcher_value, dependency)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
compute(
|
|
97
|
+
name,
|
|
98
|
+
depends_on: depends_on,
|
|
99
|
+
with: with,
|
|
100
|
+
call: call,
|
|
101
|
+
executor: executor,
|
|
102
|
+
**metadata.merge(kind: :guard, guard: true, guard_message: message || "Guard '#{name}' failed"),
|
|
103
|
+
&block
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def export(*names, from:, **metadata)
|
|
108
|
+
names.each do |name|
|
|
109
|
+
output(name, from: "#{from}.#{name}", **metadata)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def expose(*sources, as: nil, **metadata)
|
|
114
|
+
raise CompileError, "expose cannot use `as:` with multiple sources" if as && sources.size != 1
|
|
115
|
+
|
|
116
|
+
sources.each do |source|
|
|
117
|
+
output(as || source, from: source, **metadata)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def scope(name, &block)
|
|
122
|
+
raise CompileError, "scope requires a block" unless block
|
|
123
|
+
|
|
124
|
+
@scope_stack << name.to_s
|
|
125
|
+
instance_eval(&block)
|
|
126
|
+
ensure
|
|
127
|
+
@scope_stack.pop
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
alias namespace scope
|
|
131
|
+
|
|
49
132
|
def output(name, from: nil, **metadata)
|
|
50
133
|
add_node(
|
|
51
134
|
Model::OutputNode.new(
|
|
52
135
|
id: next_id,
|
|
53
136
|
name: name,
|
|
54
137
|
source: (from || name),
|
|
138
|
+
path: scoped_output_path(name),
|
|
55
139
|
metadata: with_source_location(metadata)
|
|
56
140
|
)
|
|
57
141
|
)
|
|
@@ -66,6 +150,42 @@ module Igniter
|
|
|
66
150
|
name: name,
|
|
67
151
|
contract_class: contract,
|
|
68
152
|
input_mapping: inputs,
|
|
153
|
+
path: scoped_path(name),
|
|
154
|
+
metadata: with_source_location(metadata)
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def branch(name, with:, inputs:, **metadata, &block)
|
|
160
|
+
raise CompileError, "branch :#{name} requires a block" unless block
|
|
161
|
+
raise CompileError, "branch :#{name} requires an `inputs:` hash" unless inputs.is_a?(Hash)
|
|
162
|
+
|
|
163
|
+
definition = BranchBuilder.build(&block)
|
|
164
|
+
|
|
165
|
+
add_node(
|
|
166
|
+
Model::BranchNode.new(
|
|
167
|
+
id: next_id,
|
|
168
|
+
name: name,
|
|
169
|
+
selector_dependency: with,
|
|
170
|
+
cases: definition[:cases],
|
|
171
|
+
default_contract: definition[:default_contract],
|
|
172
|
+
input_mapping: inputs,
|
|
173
|
+
path: scoped_path(name),
|
|
174
|
+
metadata: with_source_location(metadata)
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def collection(name, with:, each:, key:, mode: :collect, **metadata)
|
|
180
|
+
add_node(
|
|
181
|
+
Model::CollectionNode.new(
|
|
182
|
+
id: next_id,
|
|
183
|
+
name: name,
|
|
184
|
+
source_dependency: with,
|
|
185
|
+
contract_class: each,
|
|
186
|
+
key_name: key,
|
|
187
|
+
mode: mode,
|
|
188
|
+
path: scoped_path(name),
|
|
69
189
|
metadata: with_source_location(metadata)
|
|
70
190
|
)
|
|
71
191
|
)
|
|
@@ -90,6 +210,89 @@ module Igniter
|
|
|
90
210
|
def with_source_location(metadata)
|
|
91
211
|
metadata.merge(source_location: caller_locations(2, 1).first&.to_s)
|
|
92
212
|
end
|
|
213
|
+
|
|
214
|
+
def resolve_compute_callable(call:, executor:, metadata:, block:)
|
|
215
|
+
raise CompileError, "compute cannot accept both `call:` and `executor:`" if call && executor
|
|
216
|
+
raise CompileError, "compute cannot accept both `call:` and a block" if call && block
|
|
217
|
+
raise CompileError, "compute cannot accept both `executor:` and a block" if executor && block
|
|
218
|
+
|
|
219
|
+
if executor
|
|
220
|
+
definition = Igniter.executor_registry.fetch(executor)
|
|
221
|
+
return [definition.executor_class, definition.metadata.merge(metadata).merge(executor_key: definition.key)]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
[call || block, metadata]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def normalize_dependencies(depends_on:, with:)
|
|
228
|
+
raise CompileError, "Use either `depends_on:` or `with:`, not both" if depends_on && with
|
|
229
|
+
|
|
230
|
+
dependencies = depends_on || with
|
|
231
|
+
Array(dependencies)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def build_guard_matcher(matcher_name, matcher_value, dependency)
|
|
235
|
+
case matcher_name
|
|
236
|
+
when :eq
|
|
237
|
+
proc do |**values|
|
|
238
|
+
values.fetch(dependency) == matcher_value
|
|
239
|
+
end
|
|
240
|
+
when :in
|
|
241
|
+
allowed_values = Array(matcher_value)
|
|
242
|
+
proc do |**values|
|
|
243
|
+
allowed_values.include?(values.fetch(dependency))
|
|
244
|
+
end
|
|
245
|
+
when :matches
|
|
246
|
+
matcher = matcher_value
|
|
247
|
+
raise CompileError, "`matches:` expects a Regexp" unless matcher.is_a?(Regexp)
|
|
248
|
+
|
|
249
|
+
proc do |**values|
|
|
250
|
+
values.fetch(dependency).to_s.match?(matcher)
|
|
251
|
+
end
|
|
252
|
+
else
|
|
253
|
+
raise CompileError, "Unsupported guard matcher: #{matcher_name}"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def scoped_path(name)
|
|
258
|
+
return name.to_s if @scope_stack.empty?
|
|
259
|
+
|
|
260
|
+
"#{@scope_stack.join('.')}.#{name}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def scoped_output_path(name)
|
|
264
|
+
return "output.#{name}" if @scope_stack.empty?
|
|
265
|
+
|
|
266
|
+
"#{@scope_stack.join('.')}.output.#{name}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
class BranchBuilder
|
|
270
|
+
def self.build(&block)
|
|
271
|
+
new.tap { |builder| builder.instance_eval(&block) }.to_h
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def initialize
|
|
275
|
+
@cases = []
|
|
276
|
+
@default_contract = nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def on(match, contract:)
|
|
280
|
+
@cases << { match: match, contract: contract }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def default(contract:)
|
|
284
|
+
raise CompileError, "branch can define only one `default`" if @default_contract
|
|
285
|
+
|
|
286
|
+
@default_contract = contract
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def to_h
|
|
290
|
+
{
|
|
291
|
+
cases: @cases,
|
|
292
|
+
default_contract: @default_contract
|
|
293
|
+
}
|
|
294
|
+
end
|
|
295
|
+
end
|
|
93
296
|
end
|
|
94
297
|
end
|
|
95
298
|
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
|
data/lib/igniter/dsl.rb
CHANGED
data/lib/igniter/errors.rb
CHANGED
|
@@ -48,6 +48,17 @@ module Igniter
|
|
|
48
48
|
class ValidationError < CompileError; end
|
|
49
49
|
class CycleError < ValidationError; end
|
|
50
50
|
class InputError < Error; end
|
|
51
|
+
class CollectionInputError < Error; end
|
|
52
|
+
class CollectionKeyError < Error; end
|
|
51
53
|
class ResolutionError < Error; end
|
|
52
54
|
class CompositionError < Error; end
|
|
55
|
+
class BranchSelectionError < Error; end
|
|
56
|
+
class PendingDependencyError < Error
|
|
57
|
+
attr_reader :deferred_result
|
|
58
|
+
|
|
59
|
+
def initialize(deferred_result, message = "Dependency is pending", context: {})
|
|
60
|
+
@deferred_result = deferred_result
|
|
61
|
+
super(message, context: context)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
53
64
|
end
|
data/lib/igniter/events/bus.rb
CHANGED
|
@@ -34,6 +34,11 @@ module Igniter
|
|
|
34
34
|
def subscribe(subscriber = nil, &block)
|
|
35
35
|
@subscribers << (subscriber || block)
|
|
36
36
|
end
|
|
37
|
+
|
|
38
|
+
def restore!(events:, execution_id: nil)
|
|
39
|
+
@execution_id = execution_id if execution_id
|
|
40
|
+
@events = Array(events).map { |event| event.is_a?(Event) ? event : Event.from_h(event) }
|
|
41
|
+
end
|
|
37
42
|
end
|
|
38
43
|
end
|
|
39
44
|
end
|
data/lib/igniter/events/event.rb
CHANGED
|
@@ -16,6 +16,20 @@ module Igniter
|
|
|
16
16
|
:timestamp,
|
|
17
17
|
keyword_init: true
|
|
18
18
|
) do
|
|
19
|
+
def self.from_h(data)
|
|
20
|
+
new(
|
|
21
|
+
event_id: value_from(data, :event_id),
|
|
22
|
+
type: value_from(data, :type).to_sym,
|
|
23
|
+
execution_id: value_from(data, :execution_id),
|
|
24
|
+
node_id: value_from(data, :node_id),
|
|
25
|
+
node_name: value_from(data, :node_name)&.to_sym,
|
|
26
|
+
path: value_from(data, :path),
|
|
27
|
+
status: value_from(data, :status)&.to_sym,
|
|
28
|
+
payload: value_from(data, :payload) || {},
|
|
29
|
+
timestamp: parse_timestamp(value_from(data, :timestamp))
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
19
33
|
def to_h
|
|
20
34
|
{
|
|
21
35
|
event_id: event_id,
|
|
@@ -36,6 +50,21 @@ module Igniter
|
|
|
36
50
|
|
|
37
51
|
private
|
|
38
52
|
|
|
53
|
+
def self.parse_timestamp(value)
|
|
54
|
+
case value
|
|
55
|
+
when Time
|
|
56
|
+
value
|
|
57
|
+
when String
|
|
58
|
+
Time.iso8601(value)
|
|
59
|
+
else
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.value_from(data, key)
|
|
65
|
+
data[key] || data[key.to_s]
|
|
66
|
+
end
|
|
67
|
+
|
|
39
68
|
def serialize_value(value)
|
|
40
69
|
case value
|
|
41
70
|
when Time
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
class Executor
|
|
5
|
+
class << self
|
|
6
|
+
def inherited(subclass)
|
|
7
|
+
super
|
|
8
|
+
subclass.instance_variable_set(:@executor_inputs, executor_inputs.transform_values(&:dup))
|
|
9
|
+
subclass.instance_variable_set(:@executor_metadata, executor_metadata.dup)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def input(name, required: true, type: nil, **metadata)
|
|
13
|
+
executor_inputs[name.to_sym] = metadata.merge(required: required, type: type).compact
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def executor_inputs
|
|
17
|
+
@executor_inputs ||= {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def executor_metadata
|
|
21
|
+
@executor_metadata ||= {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def executor_key(value = nil)
|
|
25
|
+
metadata_value(:key, value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def label(value = nil)
|
|
29
|
+
metadata_value(:label, value)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def category(value = nil)
|
|
33
|
+
metadata_value(:category, value)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def summary(value = nil)
|
|
37
|
+
metadata_value(:summary, value)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def tags(*values)
|
|
41
|
+
return Array(executor_metadata[:tags]).freeze if values.empty?
|
|
42
|
+
|
|
43
|
+
executor_metadata[:tags] = values.flatten.compact.map(&:to_sym).freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def output_schema(value = nil)
|
|
47
|
+
metadata_value(:output_schema, value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def call(**dependencies)
|
|
51
|
+
new.call(**dependencies)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def metadata_value(key, value)
|
|
57
|
+
return executor_metadata[key] if value.nil?
|
|
58
|
+
|
|
59
|
+
executor_metadata[key] = value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
attr_reader :execution, :contract
|
|
64
|
+
|
|
65
|
+
def initialize(execution: nil, contract: nil)
|
|
66
|
+
@execution = execution
|
|
67
|
+
@contract = contract
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def defer(token: nil, payload: {})
|
|
71
|
+
Runtime::DeferredResult.build(token: token, payload: payload)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
class ExecutorRegistry
|
|
5
|
+
Definition = Struct.new(:key, :executor_class, :metadata, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@definitions = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def register(key, executor_class, **metadata)
|
|
12
|
+
normalized_key = key.to_s
|
|
13
|
+
raise CompileError, "executor registry key cannot be empty" if normalized_key.empty?
|
|
14
|
+
|
|
15
|
+
unless executor_class.is_a?(Class) && executor_class <= Igniter::Executor
|
|
16
|
+
raise CompileError, "Executor registry key '#{normalized_key}' must reference an Igniter::Executor subclass"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@definitions[normalized_key] = Definition.new(
|
|
20
|
+
key: normalized_key,
|
|
21
|
+
executor_class: executor_class,
|
|
22
|
+
metadata: executor_class.executor_metadata.merge(metadata).merge(key: normalized_key)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fetch(key)
|
|
27
|
+
@definitions.fetch(key.to_s)
|
|
28
|
+
rescue KeyError
|
|
29
|
+
raise CompileError, "Unknown executor registry key: #{key}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def registered?(key)
|
|
33
|
+
@definitions.key?(key.to_s)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def definitions
|
|
37
|
+
@definitions.values.sort_by(&:key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clear
|
|
41
|
+
@definitions.clear
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -19,6 +19,10 @@ module Igniter
|
|
|
19
19
|
@events.dup
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def restore!(events)
|
|
23
|
+
@events = Array(events).map { |event| event.is_a?(Igniter::Events::Event) ? event : Igniter::Events::Event.from_h(event) }
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
def snapshot
|
|
23
27
|
{
|
|
24
28
|
execution_id: execution.events.execution_id,
|
|
@@ -23,9 +23,31 @@ module Igniter
|
|
|
23
23
|
@graph.nodes.each do |node|
|
|
24
24
|
line = "- #{node.kind} #{node.path}"
|
|
25
25
|
line += " depends_on=#{node.dependencies.join(',')}" if node.dependencies.any?
|
|
26
|
+
if node.kind == :compute
|
|
27
|
+
line += " callable=#{node.callable_name}"
|
|
28
|
+
line += " guard=true" if node.guard?
|
|
29
|
+
line += " const=true" if node.const?
|
|
30
|
+
line += " executor_key=#{node.executor_key}" if node.executor_key
|
|
31
|
+
line += " label=#{node.executor_label}" if node.executor_label
|
|
32
|
+
line += " category=#{node.executor_category}" if node.executor_category
|
|
33
|
+
line += " tags=#{node.executor_tags.join(',')}" if node.executor_tags.any?
|
|
34
|
+
line += " summary=#{node.executor_summary}" if node.executor_summary
|
|
35
|
+
end
|
|
26
36
|
if node.kind == :composition
|
|
27
37
|
line += " contract=#{node.contract_class.name || 'AnonymousContract'}"
|
|
28
38
|
end
|
|
39
|
+
if node.kind == :branch
|
|
40
|
+
cases = node.cases.map { |entry| "#{entry[:match].inspect}:#{entry[:contract].name || 'AnonymousContract'}" }
|
|
41
|
+
line += " selector=#{node.selector_dependency}"
|
|
42
|
+
line += " cases=#{cases.join('|')}"
|
|
43
|
+
line += " default=#{node.default_contract.name || 'AnonymousContract'}"
|
|
44
|
+
end
|
|
45
|
+
if node.kind == :collection
|
|
46
|
+
line += " with=#{node.source_dependency}"
|
|
47
|
+
line += " each=#{node.contract_class.name || 'AnonymousContract'}"
|
|
48
|
+
line += " key=#{node.key_name}"
|
|
49
|
+
line += " mode=#{node.mode}"
|
|
50
|
+
end
|
|
29
51
|
lines << line
|
|
30
52
|
end
|
|
31
53
|
lines << "Outputs:"
|
|
@@ -43,11 +65,11 @@ module Igniter
|
|
|
43
65
|
end
|
|
44
66
|
@graph.outputs.each do |output|
|
|
45
67
|
lines << %( #{output_id(output)}["output: #{output.name}"])
|
|
46
|
-
lines << %( #{node_id(@graph.fetch_node(output.
|
|
68
|
+
lines << %( #{node_id(@graph.fetch_node(output.source_root))} --> #{output_id(output)})
|
|
47
69
|
end
|
|
48
70
|
@graph.nodes.each do |node|
|
|
49
71
|
node.dependencies.each do |dependency_name|
|
|
50
|
-
dependency_node = @graph.
|
|
72
|
+
dependency_node = @graph.fetch_dependency(dependency_name)
|
|
51
73
|
lines << %( #{node_id(dependency_node)} --> #{node_id(node)})
|
|
52
74
|
end
|
|
53
75
|
end
|
|
@@ -57,6 +79,8 @@ module Igniter
|
|
|
57
79
|
private
|
|
58
80
|
|
|
59
81
|
def node_id(node)
|
|
82
|
+
return "output_#{node.name}" if node.kind == :output
|
|
83
|
+
|
|
60
84
|
"node_#{node.name}"
|
|
61
85
|
end
|
|
62
86
|
|
|
@@ -65,7 +89,9 @@ module Igniter
|
|
|
65
89
|
end
|
|
66
90
|
|
|
67
91
|
def node_label(node)
|
|
68
|
-
"#{node.kind}: #{node.
|
|
92
|
+
return "#{node.kind}: #{node.path}" unless node.kind == :compute
|
|
93
|
+
|
|
94
|
+
"#{node.kind}: #{node.path}\\n#{node.callable_name}"
|
|
69
95
|
end
|
|
70
96
|
end
|
|
71
97
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Introspection
|
|
6
|
+
class PlanFormatter
|
|
7
|
+
def self.to_text(execution, output_names = nil)
|
|
8
|
+
new(execution, output_names).to_text
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(execution, output_names = nil)
|
|
12
|
+
@execution = execution
|
|
13
|
+
@plan = execution.plan(output_names)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_text
|
|
17
|
+
lines = []
|
|
18
|
+
lines << "Plan #{@execution.compiled_graph.name}"
|
|
19
|
+
lines << "Targets: #{format_list(@plan[:targets])}"
|
|
20
|
+
lines << "Ready: #{format_list(@plan[:ready])}"
|
|
21
|
+
lines << "Blocked: #{format_list(@plan[:blocked])}"
|
|
22
|
+
lines << "Nodes:"
|
|
23
|
+
|
|
24
|
+
@plan[:nodes].each_value do |entry|
|
|
25
|
+
lines << format_node(entry)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
lines.join("\n")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def format_node(entry)
|
|
34
|
+
line = "- #{entry[:kind]} #{entry[:path]} status=#{entry[:status]}"
|
|
35
|
+
line += " ready=true" if entry[:ready]
|
|
36
|
+
line += " blocked=true" if entry[:blocked]
|
|
37
|
+
line += " waiting_on=#{format_list(entry[:waiting_on])}" if entry[:waiting_on].any?
|
|
38
|
+
|
|
39
|
+
dependency_summary = entry[:dependencies].map do |dependency|
|
|
40
|
+
"#{dependency[:name]}(#{dependency[:status]})"
|
|
41
|
+
end
|
|
42
|
+
line += " deps=#{dependency_summary.join(',')}" if dependency_summary.any?
|
|
43
|
+
line
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def format_list(values)
|
|
47
|
+
array = Array(values)
|
|
48
|
+
return "none" if array.empty?
|
|
49
|
+
|
|
50
|
+
array.join(",")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|