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
|
@@ -9,196 +9,10 @@ module Igniter
|
|
|
9
9
|
|
|
10
10
|
def initialize(graph)
|
|
11
11
|
@graph = graph
|
|
12
|
-
@runtime_nodes_by_name = {}
|
|
13
12
|
end
|
|
14
13
|
|
|
15
14
|
def call
|
|
16
|
-
|
|
17
|
-
index_runtime_nodes!
|
|
18
|
-
validate_outputs!
|
|
19
|
-
validate_unique_paths!
|
|
20
|
-
validate_dependencies!
|
|
21
|
-
validate_callable_signatures!
|
|
22
|
-
|
|
23
|
-
self
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def runtime_nodes
|
|
27
|
-
@runtime_nodes ||= @graph.nodes.reject { |node| node.kind == :output }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def outputs
|
|
31
|
-
@outputs ||= @graph.nodes.select { |node| node.kind == :output }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def runtime_nodes_by_name
|
|
35
|
-
@runtime_nodes_by_name
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def validate_unique_ids!
|
|
41
|
-
seen = {}
|
|
42
|
-
|
|
43
|
-
@graph.nodes.each do |node|
|
|
44
|
-
if seen.key?(node.id)
|
|
45
|
-
raise validation_error(node, "Duplicate node id: #{node.id}")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
seen[node.id] = true
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def validate_unique_paths!
|
|
53
|
-
seen = {}
|
|
54
|
-
|
|
55
|
-
runtime_nodes.each do |node|
|
|
56
|
-
if seen.key?(node.path)
|
|
57
|
-
raise validation_error(node, "Duplicate node path: #{node.path}")
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
seen[node.path] = true
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
outputs.each do |output|
|
|
64
|
-
if seen.key?(output.path)
|
|
65
|
-
raise validation_error(output, "Duplicate node path: #{output.path}")
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
seen[output.path] = true
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def index_runtime_nodes!
|
|
73
|
-
runtime_nodes.each do |node|
|
|
74
|
-
raise validation_error(node, "Duplicate node name: #{node.name}") if @runtime_nodes_by_name.key?(node.name)
|
|
75
|
-
|
|
76
|
-
@runtime_nodes_by_name[node.name] = node
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def validate_outputs!
|
|
81
|
-
raise ValidationError, "Graph must define at least one output" if outputs.empty?
|
|
82
|
-
|
|
83
|
-
seen = {}
|
|
84
|
-
outputs.each do |output|
|
|
85
|
-
raise validation_error(output, "Duplicate output name: #{output.name}") if seen.key?(output.name)
|
|
86
|
-
raise validation_error(output, "Unknown output source '#{output.source}' for output '#{output.name}'") unless @runtime_nodes_by_name.key?(output.source)
|
|
87
|
-
|
|
88
|
-
seen[output.name] = true
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def validate_dependencies!
|
|
93
|
-
runtime_nodes.each do |node|
|
|
94
|
-
validate_composition_node!(node) if node.kind == :composition
|
|
95
|
-
|
|
96
|
-
node.dependencies.each do |dependency_name|
|
|
97
|
-
next if @runtime_nodes_by_name.key?(dependency_name)
|
|
98
|
-
|
|
99
|
-
raise validation_error(node, "Unknown dependency '#{dependency_name}' for node '#{node.name}'")
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def validate_composition_node!(node)
|
|
105
|
-
contract_class = node.contract_class
|
|
106
|
-
|
|
107
|
-
unless contract_class.is_a?(Class) && contract_class <= Igniter::Contract
|
|
108
|
-
raise validation_error(node, "Composition '#{node.name}' must reference an Igniter::Contract subclass")
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
unless contract_class.compiled_graph
|
|
112
|
-
raise validation_error(node, "Composition '#{node.name}' references an uncompiled contract")
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
validate_composition_input_mapping!(node, contract_class.compiled_graph)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def validate_composition_input_mapping!(node, child_graph)
|
|
119
|
-
child_input_nodes = child_graph.nodes.select { |child_node| child_node.kind == :input }
|
|
120
|
-
child_input_names = child_input_nodes.map(&:name)
|
|
121
|
-
|
|
122
|
-
unknown_inputs = node.input_mapping.keys - child_input_names
|
|
123
|
-
unless unknown_inputs.empty?
|
|
124
|
-
raise validation_error(
|
|
125
|
-
node,
|
|
126
|
-
"Composition '#{node.name}' maps unknown child inputs: #{unknown_inputs.sort.join(', ')}"
|
|
127
|
-
)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
missing_required_inputs = child_input_nodes
|
|
131
|
-
.select(&:required?)
|
|
132
|
-
.reject { |child_input| node.input_mapping.key?(child_input.name) }
|
|
133
|
-
.map(&:name)
|
|
134
|
-
|
|
135
|
-
return if missing_required_inputs.empty?
|
|
136
|
-
|
|
137
|
-
raise validation_error(
|
|
138
|
-
node,
|
|
139
|
-
"Composition '#{node.name}' is missing mappings for required child inputs: #{missing_required_inputs.sort.join(', ')}"
|
|
140
|
-
)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def validate_callable_signatures!
|
|
144
|
-
runtime_nodes.each do |node|
|
|
145
|
-
next unless node.kind == :compute
|
|
146
|
-
next unless node.callable.is_a?(Proc)
|
|
147
|
-
|
|
148
|
-
validate_proc_signature!(node)
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def validate_proc_signature!(node)
|
|
153
|
-
parameters = node.callable.parameters
|
|
154
|
-
positional_kinds = %i[req opt rest]
|
|
155
|
-
positional = parameters.select { |kind, _name| positional_kinds.include?(kind) }
|
|
156
|
-
|
|
157
|
-
unless positional.empty?
|
|
158
|
-
raise validation_error(
|
|
159
|
-
node,
|
|
160
|
-
"Compute '#{node.name}' proc must use keyword arguments for dependencies, got positional parameters"
|
|
161
|
-
)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
accepts_any_keywords = parameters.any? { |kind, _name| kind == :keyrest }
|
|
165
|
-
accepted_keywords = parameters
|
|
166
|
-
.select { |kind, _name| %i[key keyreq].include?(kind) }
|
|
167
|
-
.map(&:last)
|
|
168
|
-
required_keywords = parameters
|
|
169
|
-
.select { |kind, _name| kind == :keyreq }
|
|
170
|
-
.map(&:last)
|
|
171
|
-
|
|
172
|
-
missing_dependencies = required_keywords - node.dependencies
|
|
173
|
-
unless missing_dependencies.empty?
|
|
174
|
-
raise validation_error(
|
|
175
|
-
node,
|
|
176
|
-
"Compute '#{node.name}' requires undeclared dependencies: #{missing_dependencies.sort.join(', ')}"
|
|
177
|
-
)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
return if accepts_any_keywords
|
|
181
|
-
|
|
182
|
-
unknown_dependencies = node.dependencies - accepted_keywords
|
|
183
|
-
return if unknown_dependencies.empty?
|
|
184
|
-
|
|
185
|
-
raise validation_error(
|
|
186
|
-
node,
|
|
187
|
-
"Compute '#{node.name}' declares unsupported dependencies for its proc: #{unknown_dependencies.sort.join(', ')}"
|
|
188
|
-
)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def validation_error(node, message)
|
|
192
|
-
ValidationError.new(
|
|
193
|
-
message,
|
|
194
|
-
context: {
|
|
195
|
-
graph: @graph.name,
|
|
196
|
-
node_id: node.id,
|
|
197
|
-
node_name: node.name,
|
|
198
|
-
node_path: node.path,
|
|
199
|
-
source_location: node.source_location
|
|
200
|
-
}
|
|
201
|
-
)
|
|
15
|
+
ValidationPipeline.call(ValidationContext.new(@graph))
|
|
202
16
|
end
|
|
203
17
|
end
|
|
204
18
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Compiler
|
|
5
|
+
module Validators
|
|
6
|
+
class CallableValidator
|
|
7
|
+
def self.call(context)
|
|
8
|
+
new(context).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(context)
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
@context.runtime_nodes.each do |node|
|
|
17
|
+
next unless node.kind == :compute
|
|
18
|
+
|
|
19
|
+
validate_callable_signature!(node)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def validate_callable_signature!(node)
|
|
26
|
+
callable = node.callable
|
|
27
|
+
|
|
28
|
+
case callable
|
|
29
|
+
when Proc
|
|
30
|
+
validate_parameters_signature!(node, callable.parameters, "proc")
|
|
31
|
+
when Class
|
|
32
|
+
validate_class_callable_signature!(node, callable)
|
|
33
|
+
when Symbol, String
|
|
34
|
+
nil
|
|
35
|
+
else
|
|
36
|
+
validate_object_callable_signature!(node, callable)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_class_callable_signature!(node, callable)
|
|
41
|
+
if callable <= Igniter::Executor
|
|
42
|
+
validate_executor_inputs!(node, callable)
|
|
43
|
+
validate_parameters_signature!(node, callable.instance_method(:call).parameters, callable.name || "executor")
|
|
44
|
+
elsif callable.respond_to?(:call)
|
|
45
|
+
validate_parameters_signature!(node, callable.method(:call).parameters, callable.name || "callable class")
|
|
46
|
+
else
|
|
47
|
+
raise @context.validation_error(node, "Compute '#{node.name}' class callable must respond to `.call`")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_object_callable_signature!(node, callable)
|
|
52
|
+
unless callable.respond_to?(:call)
|
|
53
|
+
raise @context.validation_error(node, "Compute '#{node.name}' callable object must respond to `call`")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
validate_parameters_signature!(node, callable.method(:call).parameters, callable.class.name || "callable object")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_executor_inputs!(node, executor_class)
|
|
60
|
+
declared_inputs = executor_class.executor_inputs
|
|
61
|
+
required_inputs = declared_inputs.select { |_name, config| config[:required] }.keys
|
|
62
|
+
missing_dependencies = required_inputs - node.dependencies
|
|
63
|
+
|
|
64
|
+
return if missing_dependencies.empty?
|
|
65
|
+
|
|
66
|
+
raise @context.validation_error(
|
|
67
|
+
node,
|
|
68
|
+
"Compute '#{node.name}' executor requires undeclared dependencies: #{missing_dependencies.sort.join(', ')}"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_parameters_signature!(node, parameters, callable_label)
|
|
73
|
+
positional_kinds = %i[req opt rest]
|
|
74
|
+
positional = parameters.select { |kind, _name| positional_kinds.include?(kind) }
|
|
75
|
+
unless positional.empty?
|
|
76
|
+
raise @context.validation_error(
|
|
77
|
+
node,
|
|
78
|
+
"Compute '#{node.name}' #{callable_label} must use keyword arguments for dependencies, got positional parameters"
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
accepts_any_keywords = parameters.any? { |kind, _name| kind == :keyrest }
|
|
83
|
+
accepted_keywords = parameters.select { |kind, _name| %i[key keyreq].include?(kind) }.map(&:last)
|
|
84
|
+
required_keywords = parameters.select { |kind, _name| kind == :keyreq }.map(&:last)
|
|
85
|
+
|
|
86
|
+
missing_dependencies = required_keywords - node.dependencies
|
|
87
|
+
unless missing_dependencies.empty?
|
|
88
|
+
raise @context.validation_error(
|
|
89
|
+
node,
|
|
90
|
+
"Compute '#{node.name}' #{callable_label} requires undeclared dependencies: #{missing_dependencies.sort.join(', ')}"
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return if accepts_any_keywords
|
|
95
|
+
|
|
96
|
+
unknown_dependencies = node.dependencies - accepted_keywords
|
|
97
|
+
return if unknown_dependencies.empty?
|
|
98
|
+
|
|
99
|
+
raise @context.validation_error(
|
|
100
|
+
node,
|
|
101
|
+
"Compute '#{node.name}' declares unsupported dependencies for its #{callable_label}: #{unknown_dependencies.sort.join(', ')}"
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Compiler
|
|
5
|
+
module Validators
|
|
6
|
+
class DependenciesValidator
|
|
7
|
+
def self.call(context)
|
|
8
|
+
new(context).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(context)
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
@context.runtime_nodes.each do |node|
|
|
17
|
+
validate_composition_node!(node) if node.kind == :composition
|
|
18
|
+
validate_branch_node!(node) if node.kind == :branch
|
|
19
|
+
validate_collection_node!(node) if node.kind == :collection
|
|
20
|
+
|
|
21
|
+
node.dependencies.each do |dependency_name|
|
|
22
|
+
next if @context.dependency_resolvable?(dependency_name)
|
|
23
|
+
|
|
24
|
+
raise @context.validation_error(node, "Unknown dependency '#{dependency_name}' for node '#{node.name}'")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def validate_composition_node!(node)
|
|
32
|
+
contract_class = node.contract_class
|
|
33
|
+
|
|
34
|
+
unless contract_class.is_a?(Class) && contract_class <= Igniter::Contract
|
|
35
|
+
raise @context.validation_error(node, "Composition '#{node.name}' must reference an Igniter::Contract subclass")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
unless contract_class.compiled_graph
|
|
39
|
+
raise @context.validation_error(node, "Composition '#{node.name}' references an uncompiled contract")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
validate_composition_input_mapping!(node, contract_class.compiled_graph)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_composition_input_mapping!(node, child_graph)
|
|
46
|
+
child_input_nodes = child_graph.nodes.select { |child_node| child_node.kind == :input }
|
|
47
|
+
child_input_names = child_input_nodes.map(&:name)
|
|
48
|
+
|
|
49
|
+
unknown_inputs = node.input_mapping.keys - child_input_names
|
|
50
|
+
unless unknown_inputs.empty?
|
|
51
|
+
raise @context.validation_error(
|
|
52
|
+
node,
|
|
53
|
+
"Composition '#{node.name}' maps unknown child inputs: #{unknown_inputs.sort.join(', ')}"
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
missing_required_inputs = child_input_nodes
|
|
58
|
+
.select(&:required?)
|
|
59
|
+
.reject { |child_input| node.input_mapping.key?(child_input.name) }
|
|
60
|
+
.map(&:name)
|
|
61
|
+
|
|
62
|
+
return if missing_required_inputs.empty?
|
|
63
|
+
|
|
64
|
+
raise @context.validation_error(
|
|
65
|
+
node,
|
|
66
|
+
"Composition '#{node.name}' is missing mappings for required child inputs: #{missing_required_inputs.sort.join(', ')}"
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_branch_node!(node)
|
|
71
|
+
validate_branch_structure!(node)
|
|
72
|
+
|
|
73
|
+
node.possible_contracts.each do |contract_class|
|
|
74
|
+
validate_branch_contract!(node, contract_class)
|
|
75
|
+
validate_branch_input_mapping!(node, contract_class.compiled_graph)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def validate_branch_structure!(node)
|
|
80
|
+
raise @context.validation_error(node, "Branch '#{node.name}' must define at least one `on` case") if node.cases.empty?
|
|
81
|
+
raise @context.validation_error(node, "Branch '#{node.name}' must define a `default` contract") unless node.default_contract
|
|
82
|
+
|
|
83
|
+
duplicate_matches = node.cases.group_by { |entry| entry[:match] }.select { |_match, entries| entries.size > 1 }.keys
|
|
84
|
+
return if duplicate_matches.empty?
|
|
85
|
+
|
|
86
|
+
raise @context.validation_error(
|
|
87
|
+
node,
|
|
88
|
+
"Branch '#{node.name}' has duplicate case values: #{duplicate_matches.map(&:inspect).join(', ')}"
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validate_branch_contract!(node, contract_class)
|
|
93
|
+
unless contract_class.is_a?(Class) && contract_class <= Igniter::Contract
|
|
94
|
+
raise @context.validation_error(node, "Branch '#{node.name}' must reference Igniter::Contract subclasses")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
unless contract_class.compiled_graph
|
|
98
|
+
raise @context.validation_error(node, "Branch '#{node.name}' references an uncompiled contract")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_branch_input_mapping!(node, child_graph)
|
|
103
|
+
return if node.input_mapper?
|
|
104
|
+
|
|
105
|
+
child_input_nodes = child_graph.nodes.select { |child_node| child_node.kind == :input }
|
|
106
|
+
child_input_names = child_input_nodes.map(&:name)
|
|
107
|
+
|
|
108
|
+
unknown_inputs = node.input_mapping.keys - child_input_names
|
|
109
|
+
unless unknown_inputs.empty?
|
|
110
|
+
raise @context.validation_error(
|
|
111
|
+
node,
|
|
112
|
+
"Branch '#{node.name}' maps unknown child inputs: #{unknown_inputs.sort.join(', ')}"
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
missing_required_inputs = child_input_nodes
|
|
117
|
+
.select(&:required?)
|
|
118
|
+
.reject { |child_input| node.input_mapping.key?(child_input.name) }
|
|
119
|
+
.map(&:name)
|
|
120
|
+
|
|
121
|
+
return if missing_required_inputs.empty?
|
|
122
|
+
|
|
123
|
+
raise @context.validation_error(
|
|
124
|
+
node,
|
|
125
|
+
"Branch '#{node.name}' is missing mappings for required child inputs: #{missing_required_inputs.sort.join(', ')}"
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_collection_node!(node)
|
|
130
|
+
unless node.contract_class.is_a?(Class) && node.contract_class <= Igniter::Contract
|
|
131
|
+
raise @context.validation_error(node, "Collection '#{node.name}' must reference an Igniter::Contract subclass")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
unless node.contract_class.compiled_graph
|
|
135
|
+
raise @context.validation_error(node, "Collection '#{node.name}' references an uncompiled contract")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
unless %i[collect fail_fast].include?(node.mode)
|
|
139
|
+
raise @context.validation_error(node, "Collection '#{node.name}' mode must be `:collect` or `:fail_fast`")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
child_input_names = node.contract_class.compiled_graph.nodes.select { |child_node| child_node.kind == :input }.map(&:name)
|
|
143
|
+
return if child_input_names.include?(node.key_name)
|
|
144
|
+
|
|
145
|
+
raise @context.validation_error(
|
|
146
|
+
node,
|
|
147
|
+
"Collection '#{node.name}' key '#{node.key_name}' must be a child contract input"
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Compiler
|
|
5
|
+
module Validators
|
|
6
|
+
class OutputsValidator
|
|
7
|
+
def self.call(context)
|
|
8
|
+
new(context).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(context)
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
raise ValidationError, "Graph must define at least one output" if @context.outputs.empty?
|
|
17
|
+
|
|
18
|
+
@context.outputs.each { |output| validate_output_source!(output) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def validate_output_source!(output)
|
|
24
|
+
if output.composition_output?
|
|
25
|
+
validate_nested_output_source!(output)
|
|
26
|
+
else
|
|
27
|
+
return if @context.runtime_nodes_by_name.key?(output.source)
|
|
28
|
+
|
|
29
|
+
raise @context.validation_error(output, "Unknown output source '#{output.source}' for output '#{output.name}'")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_nested_output_source!(output)
|
|
34
|
+
parent_node = @context.runtime_nodes_by_name[output.source_root]
|
|
35
|
+
unless %i[composition branch].include?(parent_node&.kind)
|
|
36
|
+
raise @context.validation_error(
|
|
37
|
+
output,
|
|
38
|
+
"Output '#{output.name}' references unknown nested source '#{output.source}'"
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
validate_nested_output_presence!(output, parent_node)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_nested_output_presence!(output, parent_node)
|
|
46
|
+
child_graphs =
|
|
47
|
+
case parent_node.kind
|
|
48
|
+
when :composition
|
|
49
|
+
[parent_node.contract_class.compiled_graph]
|
|
50
|
+
when :branch
|
|
51
|
+
parent_node.possible_contracts.map(&:compiled_graph)
|
|
52
|
+
else
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
return if child_graphs.all? { |graph| graph.outputs_by_name.key?(output.child_output_name) }
|
|
57
|
+
|
|
58
|
+
raise @context.validation_error(
|
|
59
|
+
output,
|
|
60
|
+
"Output '#{output.name}' references unknown child output '#{output.child_output_name}' on nested source '#{parent_node.name}'"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Compiler
|
|
5
|
+
module Validators
|
|
6
|
+
class TypeCompatibilityValidator
|
|
7
|
+
def self.call(context)
|
|
8
|
+
new(context).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(context)
|
|
12
|
+
@context = context
|
|
13
|
+
@resolver = TypeResolver.new(context)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
validate_executor_dependency_types!
|
|
18
|
+
validate_composition_input_types!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def validate_executor_dependency_types!
|
|
24
|
+
@context.runtime_nodes.each do |node|
|
|
25
|
+
next unless node.kind == :compute
|
|
26
|
+
next unless node.callable.is_a?(Class) && node.callable <= Igniter::Executor
|
|
27
|
+
|
|
28
|
+
node.callable.executor_inputs.each do |dependency_name, config|
|
|
29
|
+
next unless node.dependencies.include?(dependency_name)
|
|
30
|
+
|
|
31
|
+
validate_edge_type!(
|
|
32
|
+
node: node,
|
|
33
|
+
dependency_name: dependency_name,
|
|
34
|
+
target_type: config[:type],
|
|
35
|
+
label: "executor input '#{dependency_name}'"
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_composition_input_types!
|
|
42
|
+
@context.runtime_nodes.each do |node|
|
|
43
|
+
next unless node.kind == :composition
|
|
44
|
+
|
|
45
|
+
child_inputs = node.contract_class.compiled_graph.nodes.select { |child_node| child_node.kind == :input }
|
|
46
|
+
child_inputs.each do |child_input|
|
|
47
|
+
dependency_name = node.input_mapping[child_input.name]
|
|
48
|
+
next unless dependency_name
|
|
49
|
+
|
|
50
|
+
validate_edge_type!(
|
|
51
|
+
node: node,
|
|
52
|
+
dependency_name: dependency_name,
|
|
53
|
+
target_type: child_input.type,
|
|
54
|
+
label: "composition input '#{child_input.name}'"
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_edge_type!(node:, dependency_name:, target_type:, label:)
|
|
61
|
+
return unless target_type
|
|
62
|
+
|
|
63
|
+
unless TypeSystem.supported?(target_type)
|
|
64
|
+
raise @context.validation_error(node, "Unsupported target type '#{target_type}' for #{label}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
source_type = @resolver.call(dependency_name)
|
|
68
|
+
return if source_type.nil?
|
|
69
|
+
|
|
70
|
+
unless TypeSystem.supported?(source_type)
|
|
71
|
+
raise @context.validation_error(node, "Unsupported source type '#{source_type}' for dependency '#{dependency_name}'")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return if TypeSystem.compatible?(source_type, target_type)
|
|
75
|
+
|
|
76
|
+
raise @context.validation_error(
|
|
77
|
+
node,
|
|
78
|
+
"Type mismatch for #{label}: dependency '#{dependency_name}' is #{source_type}, expected #{target_type}"
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Compiler
|
|
5
|
+
module Validators
|
|
6
|
+
class UniquenessValidator
|
|
7
|
+
def self.call(context)
|
|
8
|
+
new(context).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(context)
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
validate_unique_ids!
|
|
17
|
+
validate_unique_names!
|
|
18
|
+
validate_unique_paths!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def validate_unique_ids!
|
|
24
|
+
seen = {}
|
|
25
|
+
@context.graph.nodes.each do |node|
|
|
26
|
+
raise @context.validation_error(node, "Duplicate node id: #{node.id}") if seen.key?(node.id)
|
|
27
|
+
|
|
28
|
+
seen[node.id] = true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def validate_unique_names!
|
|
33
|
+
seen_runtime = {}
|
|
34
|
+
@context.runtime_nodes.each do |node|
|
|
35
|
+
raise @context.validation_error(node, "Duplicate node name: #{node.name}") if seen_runtime.key?(node.name)
|
|
36
|
+
|
|
37
|
+
seen_runtime[node.name] = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
seen_outputs = {}
|
|
41
|
+
@context.outputs.each do |output|
|
|
42
|
+
raise @context.validation_error(output, "Duplicate output name: #{output.name}") if seen_outputs.key?(output.name)
|
|
43
|
+
|
|
44
|
+
seen_outputs[output.name] = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate_unique_paths!
|
|
49
|
+
seen = {}
|
|
50
|
+
|
|
51
|
+
(@context.runtime_nodes + @context.outputs).each do |node|
|
|
52
|
+
raise @context.validation_error(node, "Duplicate node path: #{node.path}") if seen.key?(node.path)
|
|
53
|
+
|
|
54
|
+
seen[node.path] = true
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/igniter/compiler.rb
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "compiler/compiled_graph"
|
|
4
|
+
require_relative "compiler/validation_context"
|
|
5
|
+
require_relative "compiler/type_resolver"
|
|
6
|
+
require_relative "compiler/validators/uniqueness_validator"
|
|
7
|
+
require_relative "compiler/validators/outputs_validator"
|
|
8
|
+
require_relative "compiler/validators/dependencies_validator"
|
|
9
|
+
require_relative "compiler/validators/callable_validator"
|
|
10
|
+
require_relative "compiler/validators/type_compatibility_validator"
|
|
11
|
+
require_relative "compiler/validation_pipeline"
|
|
4
12
|
require_relative "compiler/validator"
|
|
5
13
|
require_relative "compiler/graph_compiler"
|
|
6
14
|
|