igniter-contracts 0.5.2
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 +7 -0
- data/README.md +239 -0
- data/lib/igniter/contracts/api.rb +92 -0
- data/lib/igniter/contracts/assembly/baseline_pack.rb +141 -0
- data/lib/igniter/contracts/assembly/const_pack.rb +29 -0
- data/lib/igniter/contracts/assembly/dsl_keyword.rb +21 -0
- data/lib/igniter/contracts/assembly/hook_result_policies.rb +47 -0
- data/lib/igniter/contracts/assembly/hook_spec.rb +73 -0
- data/lib/igniter/contracts/assembly/hook_specs.rb +74 -0
- data/lib/igniter/contracts/assembly/kernel.rb +220 -0
- data/lib/igniter/contracts/assembly/node_type.rb +26 -0
- data/lib/igniter/contracts/assembly/ordered_registry.rb +55 -0
- data/lib/igniter/contracts/assembly/pack.rb +13 -0
- data/lib/igniter/contracts/assembly/pack_manifest.rb +131 -0
- data/lib/igniter/contracts/assembly/path_access.rb +76 -0
- data/lib/igniter/contracts/assembly/profile.rb +133 -0
- data/lib/igniter/contracts/assembly/project_pack.rb +42 -0
- data/lib/igniter/contracts/assembly/registry.rb +57 -0
- data/lib/igniter/contracts/assembly/step_result_pack.rb +42 -0
- data/lib/igniter/contracts/assembly.rb +18 -0
- data/lib/igniter/contracts/contract.rb +135 -0
- data/lib/igniter/contracts/contractable.rb +288 -0
- data/lib/igniter/contracts/environment.rb +51 -0
- data/lib/igniter/contracts/errors.rb +47 -0
- data/lib/igniter/contracts/execution/baseline_normalizers.rb +23 -0
- data/lib/igniter/contracts/execution/baseline_runtime.rb +55 -0
- data/lib/igniter/contracts/execution/baseline_validators.rb +113 -0
- data/lib/igniter/contracts/execution/builder.rb +43 -0
- data/lib/igniter/contracts/execution/compilation_report.rb +46 -0
- data/lib/igniter/contracts/execution/compiled_graph.rb +21 -0
- data/lib/igniter/contracts/execution/compiler.rb +66 -0
- data/lib/igniter/contracts/execution/const_runtime.rb +15 -0
- data/lib/igniter/contracts/execution/diagnostics.rb +24 -0
- data/lib/igniter/contracts/execution/diagnostics_report.rb +40 -0
- data/lib/igniter/contracts/execution/diagnostics_section.rb +37 -0
- data/lib/igniter/contracts/execution/effect_invocation.rb +26 -0
- data/lib/igniter/contracts/execution/execution_request.rb +28 -0
- data/lib/igniter/contracts/execution/execution_result.rb +32 -0
- data/lib/igniter/contracts/execution/inline_executor.rb +19 -0
- data/lib/igniter/contracts/execution/mutable_named_values.rb +52 -0
- data/lib/igniter/contracts/execution/named_values.rb +48 -0
- data/lib/igniter/contracts/execution/operation.rb +42 -0
- data/lib/igniter/contracts/execution/runtime.rb +43 -0
- data/lib/igniter/contracts/execution/step_result.rb +51 -0
- data/lib/igniter/contracts/execution/step_result_diagnostics.rb +35 -0
- data/lib/igniter/contracts/execution/step_result_runtime.rb +51 -0
- data/lib/igniter/contracts/execution/step_result_validators.rb +44 -0
- data/lib/igniter/contracts/execution/structured_dump.rb +49 -0
- data/lib/igniter/contracts/execution/validation_finding.rb +28 -0
- data/lib/igniter/contracts/execution/validation_report.rb +46 -0
- data/lib/igniter/contracts/execution.rb +28 -0
- data/lib/igniter/contracts.rb +54 -0
- data/lib/igniter/lang/backend.rb +19 -0
- data/lib/igniter/lang/backends/ruby.rb +42 -0
- data/lib/igniter/lang/diagnostic_payload.rb +174 -0
- data/lib/igniter/lang/metadata_carrier_manifest.rb +112 -0
- data/lib/igniter/lang/metadata_manifest.rb +128 -0
- data/lib/igniter/lang/receipt_payload.rb +152 -0
- data/lib/igniter/lang/schema_compatibility_diagnostic.rb +300 -0
- data/lib/igniter/lang/types.rb +84 -0
- data/lib/igniter/lang/verification_report.rb +226 -0
- data/lib/igniter/lang.rb +27 -0
- data/lib/igniter-contracts.rb +3 -0
- metadata +103 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Contractable
|
|
6
|
+
Observation = Struct.new(:name, :value, :metadata, keyword_init: true) do
|
|
7
|
+
def initialize(name:, value:, metadata: {})
|
|
8
|
+
super(name: name.to_sym, value: value, metadata: metadata.dup.freeze)
|
|
9
|
+
freeze
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_h
|
|
13
|
+
{
|
|
14
|
+
name: name,
|
|
15
|
+
value: value,
|
|
16
|
+
metadata: metadata.dup
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Result = Struct.new(:status, :outputs, :observations, :error, :metadata, keyword_init: true) do
|
|
22
|
+
def initialize(status:, outputs: {}, observations: [], error: nil, metadata: {})
|
|
23
|
+
super(
|
|
24
|
+
status: status.to_sym,
|
|
25
|
+
outputs: outputs.transform_keys(&:to_sym).freeze,
|
|
26
|
+
observations: observations.map { |observation| normalize_observation(observation) }.freeze,
|
|
27
|
+
error: error,
|
|
28
|
+
metadata: metadata.dup.freeze
|
|
29
|
+
)
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def success?
|
|
34
|
+
status == :success
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def failure?
|
|
38
|
+
!success?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_h
|
|
42
|
+
{
|
|
43
|
+
status: status,
|
|
44
|
+
success: success?,
|
|
45
|
+
outputs: outputs,
|
|
46
|
+
observations: observations.map(&:to_h),
|
|
47
|
+
error: error,
|
|
48
|
+
metadata: metadata.dup
|
|
49
|
+
}.compact
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def normalize_observation(observation)
|
|
55
|
+
return observation if observation.is_a?(Observation)
|
|
56
|
+
|
|
57
|
+
Observation.new(**observation.to_h)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
Definition = Struct.new(:method_name, :inputs, :outputs, :role, :stage, :metadata, keyword_init: true) do
|
|
62
|
+
def initialize(method_name:, inputs: [], outputs: [], role: nil, stage: nil, metadata: {})
|
|
63
|
+
super(
|
|
64
|
+
method_name: method_name.to_sym,
|
|
65
|
+
inputs: inputs.map(&:to_sym).freeze,
|
|
66
|
+
outputs: outputs.map(&:to_sym).freeze,
|
|
67
|
+
role: role&.to_sym,
|
|
68
|
+
stage: stage&.to_sym,
|
|
69
|
+
metadata: metadata.dup.freeze
|
|
70
|
+
)
|
|
71
|
+
freeze
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_h
|
|
75
|
+
{
|
|
76
|
+
method_name: method_name,
|
|
77
|
+
inputs: inputs,
|
|
78
|
+
outputs: outputs,
|
|
79
|
+
role: role,
|
|
80
|
+
stage: stage,
|
|
81
|
+
metadata: metadata.dup
|
|
82
|
+
}.compact
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class DefinitionBuilder
|
|
87
|
+
attr_reader :method_name, :inputs, :outputs, :metadata, :role_value, :stage_value
|
|
88
|
+
|
|
89
|
+
def initialize(method_name)
|
|
90
|
+
@method_name = method_name.to_sym
|
|
91
|
+
@inputs = []
|
|
92
|
+
@outputs = []
|
|
93
|
+
@metadata = {}
|
|
94
|
+
@role_value = nil
|
|
95
|
+
@stage_value = nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def input(name, **)
|
|
99
|
+
inputs << name.to_sym
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def output(name, **)
|
|
103
|
+
outputs << name.to_sym
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def role(value)
|
|
107
|
+
@role_value = value.to_sym
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def stage(value)
|
|
111
|
+
@stage_value = value.to_sym
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def meta(key, value)
|
|
115
|
+
metadata[key.to_sym] = value
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build
|
|
119
|
+
Definition.new(
|
|
120
|
+
method_name: method_name,
|
|
121
|
+
inputs: inputs,
|
|
122
|
+
outputs: outputs,
|
|
123
|
+
role: role_value,
|
|
124
|
+
stage: stage_value,
|
|
125
|
+
metadata: metadata
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class ExecutionContext
|
|
131
|
+
attr_reader :observations
|
|
132
|
+
|
|
133
|
+
def initialize
|
|
134
|
+
@observations = []
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def observe(name, metadata: {})
|
|
138
|
+
value = yield
|
|
139
|
+
observations << Observation.new(name: name, value: value, metadata: metadata)
|
|
140
|
+
value
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
module InstanceMethods
|
|
145
|
+
def observe(name, metadata: {}, &block)
|
|
146
|
+
raise Error, "observe requires a block" unless block
|
|
147
|
+
raise Error, "observe can only be used during a contractable call" unless @__igniter_contractable_context
|
|
148
|
+
|
|
149
|
+
@__igniter_contractable_context.observe(name, metadata: metadata, &block)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def success(**outputs)
|
|
153
|
+
Result.new(status: :success, outputs: outputs, observations: current_observations)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def failure(code:, message:, details: {})
|
|
157
|
+
Result.new(
|
|
158
|
+
status: :failure,
|
|
159
|
+
outputs: {},
|
|
160
|
+
observations: current_observations,
|
|
161
|
+
error: { code: code.to_sym, message: message, details: details }
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def current_observations
|
|
168
|
+
@__igniter_contractable_context&.observations || []
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
module ClassMethods
|
|
173
|
+
def contractable(method_name, &block)
|
|
174
|
+
builder = DefinitionBuilder.new(method_name)
|
|
175
|
+
builder.instance_eval(&block) if block
|
|
176
|
+
@__igniter_contractable_definition = builder.build
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def contractable_definition
|
|
180
|
+
@__igniter_contractable_definition || Definition.new(method_name: :call)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class << self
|
|
185
|
+
def included(base)
|
|
186
|
+
base.extend(ClassMethods)
|
|
187
|
+
base.include(InstanceMethods)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def invoke(target, **inputs)
|
|
191
|
+
instance = target.is_a?(Class) ? target.new : target
|
|
192
|
+
definition = instance.class.contractable_definition
|
|
193
|
+
input_error = validate_inputs(definition, inputs)
|
|
194
|
+
return input_error if input_error
|
|
195
|
+
|
|
196
|
+
context = ExecutionContext.new
|
|
197
|
+
instance.instance_variable_set(:@__igniter_contractable_context, context)
|
|
198
|
+
raw = instance.public_send(definition.method_name, **inputs)
|
|
199
|
+
result = normalize_result(raw, observations: context.observations)
|
|
200
|
+
result = with_definition_metadata(result, definition)
|
|
201
|
+
validate_outputs(definition, result)
|
|
202
|
+
rescue StandardError => e
|
|
203
|
+
Result.new(
|
|
204
|
+
status: :failure,
|
|
205
|
+
outputs: {},
|
|
206
|
+
observations: context&.observations || [],
|
|
207
|
+
metadata: definition ? definition_metadata(definition) : {},
|
|
208
|
+
error: { code: :contractable_error, message: e.message, class: e.class.name }
|
|
209
|
+
)
|
|
210
|
+
ensure
|
|
211
|
+
instance&.remove_instance_variable(:@__igniter_contractable_context) if instance&.instance_variable_defined?(:@__igniter_contractable_context)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def contractable?(target)
|
|
215
|
+
klass = target.is_a?(Class) ? target : target.class
|
|
216
|
+
klass.respond_to?(:contractable_definition)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
def validate_inputs(definition, inputs)
|
|
222
|
+
missing_inputs = definition.inputs - inputs.keys.map(&:to_sym)
|
|
223
|
+
return if missing_inputs.empty?
|
|
224
|
+
|
|
225
|
+
Result.new(
|
|
226
|
+
status: :failure,
|
|
227
|
+
metadata: definition_metadata(definition),
|
|
228
|
+
error: {
|
|
229
|
+
code: :contractable_missing_inputs,
|
|
230
|
+
message: "Missing contractable inputs: #{missing_inputs.join(", ")}",
|
|
231
|
+
inputs: missing_inputs
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def validate_outputs(definition, result)
|
|
237
|
+
return result unless result.success?
|
|
238
|
+
return result if definition.outputs.empty?
|
|
239
|
+
|
|
240
|
+
missing_outputs = definition.outputs - result.outputs.keys
|
|
241
|
+
return result if missing_outputs.empty?
|
|
242
|
+
|
|
243
|
+
Result.new(
|
|
244
|
+
status: :failure,
|
|
245
|
+
outputs: result.outputs,
|
|
246
|
+
observations: result.observations,
|
|
247
|
+
metadata: result.metadata.merge(definition_metadata(definition)),
|
|
248
|
+
error: {
|
|
249
|
+
code: :contractable_missing_outputs,
|
|
250
|
+
message: "Missing contractable outputs: #{missing_outputs.join(", ")}",
|
|
251
|
+
outputs: missing_outputs
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def with_definition_metadata(result, definition)
|
|
257
|
+
metadata = definition_metadata(definition)
|
|
258
|
+
return result if metadata.empty?
|
|
259
|
+
|
|
260
|
+
Result.new(
|
|
261
|
+
status: result.status,
|
|
262
|
+
outputs: result.outputs,
|
|
263
|
+
observations: result.observations,
|
|
264
|
+
error: result.error,
|
|
265
|
+
metadata: result.metadata.merge(metadata)
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def definition_metadata(definition)
|
|
270
|
+
metadata = definition.metadata.dup
|
|
271
|
+
metadata[:role] = definition.role if definition.role
|
|
272
|
+
metadata[:stage] = definition.stage if definition.stage
|
|
273
|
+
metadata
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def normalize_result(value, observations:)
|
|
277
|
+
return value if value.is_a?(Result)
|
|
278
|
+
|
|
279
|
+
if value.respond_to?(:to_h)
|
|
280
|
+
Result.new(status: :success, outputs: value.to_h, observations: observations)
|
|
281
|
+
else
|
|
282
|
+
Result.new(status: :success, outputs: { value: value }, observations: observations)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
class Environment
|
|
6
|
+
attr_reader :profile
|
|
7
|
+
|
|
8
|
+
def initialize(profile:)
|
|
9
|
+
@profile = profile
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def compile(&block)
|
|
13
|
+
Contracts.compile(profile: profile, &block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validation_report(&block)
|
|
17
|
+
Contracts.validation_report(profile: profile, &block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def compilation_report(&block)
|
|
21
|
+
Contracts.compilation_report(profile: profile, &block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute(compiled_graph, inputs:)
|
|
25
|
+
Contracts.execute(compiled_graph, inputs: inputs, profile: profile)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def execute_with(executor_name, compiled_graph, inputs:, runtime: Execution::Runtime)
|
|
29
|
+
Contracts.execute_with(
|
|
30
|
+
executor_name,
|
|
31
|
+
compiled_graph,
|
|
32
|
+
inputs: inputs,
|
|
33
|
+
profile: profile,
|
|
34
|
+
runtime: runtime
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def run(inputs:, &block)
|
|
39
|
+
execute(compile(&block), inputs: inputs)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def diagnose(result)
|
|
43
|
+
Contracts.diagnose(result, profile: profile)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def apply_effect(effect_name, payload:, context: {})
|
|
47
|
+
Contracts.apply_effect(effect_name, payload: payload, context: context, profile: profile)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
Error = Class.new(StandardError)
|
|
6
|
+
|
|
7
|
+
class ValidationError < Error
|
|
8
|
+
attr_reader :findings
|
|
9
|
+
|
|
10
|
+
def initialize(message = nil, findings: [])
|
|
11
|
+
@findings = Array(findings).freeze
|
|
12
|
+
super(message || default_message)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def default_message
|
|
18
|
+
return "validation failed" if findings.empty?
|
|
19
|
+
|
|
20
|
+
findings.map(&:message).join("; ")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
public
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{
|
|
27
|
+
message: message,
|
|
28
|
+
findings: findings.map(&:to_h)
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
FrozenKernelError = Class.new(Error)
|
|
34
|
+
FrozenRegistryError = Class.new(Error)
|
|
35
|
+
DuplicateRegistrationError = Class.new(Error)
|
|
36
|
+
UnknownDslKeywordError = Class.new(Error)
|
|
37
|
+
UnknownNodeKindError = Class.new(Error)
|
|
38
|
+
UnknownEffectError = Class.new(Error)
|
|
39
|
+
UnknownExecutorError = Class.new(Error)
|
|
40
|
+
ProfileMismatchError = Class.new(Error)
|
|
41
|
+
IncompletePackError = Class.new(Error)
|
|
42
|
+
UnknownPackDependencyError = Class.new(Error)
|
|
43
|
+
CircularPackDependencyError = Class.new(Error)
|
|
44
|
+
InvalidHookImplementationError = Class.new(Error)
|
|
45
|
+
InvalidHookResultError = Class.new(Error)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Execution
|
|
6
|
+
module BaselineNormalizers
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def normalize_operation_attributes(operations:, profile: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
10
|
+
operations.map do |operation|
|
|
11
|
+
attributes = operation.attributes
|
|
12
|
+
normalized_attributes = attributes.dup
|
|
13
|
+
|
|
14
|
+
next operation unless normalized_attributes.key?(:depends_on)
|
|
15
|
+
|
|
16
|
+
normalized_attributes[:depends_on] = Array(normalized_attributes[:depends_on]).map(&:to_sym)
|
|
17
|
+
operation.with_attributes(normalized_attributes)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Execution
|
|
6
|
+
module BaselineRuntime
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def handle_input(operation:, inputs:, **)
|
|
10
|
+
inputs.fetch(operation.name)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def handle_compute(operation:, state:, **)
|
|
14
|
+
callable = operation.attributes[:callable]
|
|
15
|
+
kwargs = resolve_dependency_values(operation, state: state)
|
|
16
|
+
callable.call(**kwargs)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def handle_effect(operation:, state:, profile:, **)
|
|
20
|
+
callable = operation.attributes[:callable]
|
|
21
|
+
effect_name = operation.attributes.fetch(:using).to_sym
|
|
22
|
+
dependency_values = resolve_dependency_values(operation, state: state)
|
|
23
|
+
payload = callable.call(**dependency_values)
|
|
24
|
+
invocation = EffectInvocation.new(
|
|
25
|
+
payload: payload,
|
|
26
|
+
context: {
|
|
27
|
+
node_name: operation.name,
|
|
28
|
+
effect_name: effect_name,
|
|
29
|
+
dependencies: dependency_values
|
|
30
|
+
},
|
|
31
|
+
profile: profile
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
profile.effect(effect_name).call(invocation: invocation)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handle_output(operation:, state:, **)
|
|
38
|
+
state.fetch(operation.name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def unsupported(kind)
|
|
42
|
+
lambda do |**|
|
|
43
|
+
raise NotImplementedError, "#{kind} runtime handler is not implemented in the baseline runtime yet"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_dependency_values(operation, state:)
|
|
48
|
+
Array(operation.attributes[:depends_on]).each_with_object({}) do |dependency, memo|
|
|
49
|
+
memo[dependency.to_sym] = state.fetch(dependency.to_sym)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Execution
|
|
6
|
+
module BaselineValidators
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def validate_uniqueness(operations:, profile: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
10
|
+
names = operations.reject(&:output?).map(&:name)
|
|
11
|
+
duplicates = names.group_by(&:itself).select { |_name, entries| entries.length > 1 }.keys
|
|
12
|
+
return [] if duplicates.empty?
|
|
13
|
+
|
|
14
|
+
[ValidationFinding.new(
|
|
15
|
+
code: :duplicate_node_names,
|
|
16
|
+
message: "duplicate node names: #{duplicates.map(&:to_s).join(", ")}",
|
|
17
|
+
subjects: duplicates
|
|
18
|
+
)]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate_outputs(operations:, profile: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
22
|
+
available = operations.reject(&:output?).map(&:name)
|
|
23
|
+
missing = operations.select(&:output?)
|
|
24
|
+
.map(&:name)
|
|
25
|
+
.reject { |name| available.include?(name) }
|
|
26
|
+
return [] if missing.empty?
|
|
27
|
+
|
|
28
|
+
[ValidationFinding.new(
|
|
29
|
+
code: :missing_output_targets,
|
|
30
|
+
message: "output targets are not defined: #{missing.map(&:to_s).join(", ")}",
|
|
31
|
+
subjects: missing
|
|
32
|
+
)]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate_dependencies(operations:, profile: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
36
|
+
available = operations.reject(&:output?).map(&:name)
|
|
37
|
+
missing = operations.select { |operation| operation.kind == :compute }
|
|
38
|
+
.flat_map { |operation| Array(operation.attributes[:depends_on]) }
|
|
39
|
+
.map(&:to_sym)
|
|
40
|
+
.reject { |name| available.include?(name) }
|
|
41
|
+
.uniq
|
|
42
|
+
return [] if missing.empty?
|
|
43
|
+
|
|
44
|
+
[ValidationFinding.new(
|
|
45
|
+
code: :missing_compute_dependencies,
|
|
46
|
+
message: "compute dependencies are not defined: #{missing.map(&:to_s).join(", ")}",
|
|
47
|
+
subjects: missing
|
|
48
|
+
)]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_callables(operations:, profile: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
52
|
+
missing = operations.select { |operation| operation.kind == :compute }
|
|
53
|
+
.reject { |operation| operation.attributes[:callable].respond_to?(:call) }
|
|
54
|
+
.map(&:name)
|
|
55
|
+
return [] if missing.empty?
|
|
56
|
+
|
|
57
|
+
[ValidationFinding.new(
|
|
58
|
+
code: :missing_compute_callable,
|
|
59
|
+
message: "compute nodes require a callable: #{missing.map(&:to_s).join(", ")}",
|
|
60
|
+
subjects: missing
|
|
61
|
+
)]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_effect_dependencies(operations:, profile: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
65
|
+
available = operations.reject(&:output?).map(&:name)
|
|
66
|
+
missing = operations.select { |operation| operation.kind == :effect }
|
|
67
|
+
.flat_map { |operation| Array(operation.attributes[:depends_on]) }
|
|
68
|
+
.map(&:to_sym)
|
|
69
|
+
.reject { |name| available.include?(name) }
|
|
70
|
+
.uniq
|
|
71
|
+
return [] if missing.empty?
|
|
72
|
+
|
|
73
|
+
[ValidationFinding.new(
|
|
74
|
+
code: :missing_effect_dependencies,
|
|
75
|
+
message: "effect dependencies are not defined: #{missing.map(&:to_s).join(", ")}",
|
|
76
|
+
subjects: missing
|
|
77
|
+
)]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_effect_payload_builders(operations:, profile: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
81
|
+
missing = operations.select { |operation| operation.kind == :effect }
|
|
82
|
+
.reject { |operation| operation.attributes[:callable].respond_to?(:call) }
|
|
83
|
+
.map(&:name)
|
|
84
|
+
return [] if missing.empty?
|
|
85
|
+
|
|
86
|
+
[ValidationFinding.new(
|
|
87
|
+
code: :missing_effect_payload_builder,
|
|
88
|
+
message: "effect nodes require a payload callable: #{missing.map(&:to_s).join(", ")}",
|
|
89
|
+
subjects: missing
|
|
90
|
+
)]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_effect_adapters(operations:, profile:)
|
|
94
|
+
missing = operations.select { |operation| operation.kind == :effect }
|
|
95
|
+
.map { |operation| operation.attributes[:using] }
|
|
96
|
+
.reject { |effect_name| profile.supports_effect?(effect_name) }
|
|
97
|
+
.uniq
|
|
98
|
+
return [] if missing.empty?
|
|
99
|
+
|
|
100
|
+
[ValidationFinding.new(
|
|
101
|
+
code: :unknown_effect_adapters,
|
|
102
|
+
message: "effect adapters are not registered in profile: #{missing.map(&:to_s).join(", ")}",
|
|
103
|
+
subjects: missing
|
|
104
|
+
)]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_types(operations:, profile: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
108
|
+
[]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Execution
|
|
6
|
+
class Builder
|
|
7
|
+
def self.build(profile:, &block)
|
|
8
|
+
builder = new(profile: profile)
|
|
9
|
+
builder.instance_eval(&block)
|
|
10
|
+
builder
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :profile, :operations
|
|
14
|
+
|
|
15
|
+
def initialize(profile:)
|
|
16
|
+
@profile = profile
|
|
17
|
+
@operations = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_operation(kind:, name:, **attributes)
|
|
21
|
+
normalized_kind = kind.to_sym
|
|
22
|
+
unless profile.supports_node_kind?(normalized_kind)
|
|
23
|
+
raise UnknownNodeKindError,
|
|
24
|
+
"unknown node kind #{normalized_kind}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
operations << Operation.new(kind: normalized_kind, name: name, attributes: attributes)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
31
|
+
keyword = profile.dsl_keyword(name)
|
|
32
|
+
keyword.call(*args, builder: self, **kwargs, &block)
|
|
33
|
+
rescue KeyError
|
|
34
|
+
raise UnknownDslKeywordError, "unknown DSL keyword #{name}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def respond_to_missing?(name, include_private = false)
|
|
38
|
+
profile.dsl_keywords.key?(name.to_sym) || super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Execution
|
|
6
|
+
class CompilationReport
|
|
7
|
+
attr_reader :operations, :validation_report, :compiled_graph, :profile_fingerprint
|
|
8
|
+
|
|
9
|
+
def initialize(operations:, validation_report:, compiled_graph:, profile_fingerprint:)
|
|
10
|
+
@operations = operations.freeze
|
|
11
|
+
@validation_report = validation_report
|
|
12
|
+
@compiled_graph = compiled_graph
|
|
13
|
+
@profile_fingerprint = profile_fingerprint
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ok?
|
|
18
|
+
validation_report.ok?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def invalid?
|
|
22
|
+
validation_report.invalid?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def findings
|
|
26
|
+
validation_report.findings
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_compiled_graph
|
|
30
|
+
validation_report.raise_if_invalid!
|
|
31
|
+
compiled_graph
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
{
|
|
36
|
+
operations: StructuredDump.dump(operations),
|
|
37
|
+
validation_report: validation_report.to_h,
|
|
38
|
+
compiled_graph: StructuredDump.dump(compiled_graph),
|
|
39
|
+
profile_fingerprint: profile_fingerprint,
|
|
40
|
+
ok: ok?
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Execution
|
|
6
|
+
CompiledGraph = Struct.new(:operations, :profile_fingerprint, keyword_init: true) do
|
|
7
|
+
def initialize(operations:, profile_fingerprint:)
|
|
8
|
+
frozen_operations = operations.freeze
|
|
9
|
+
super(operations: frozen_operations, profile_fingerprint: profile_fingerprint)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_h
|
|
13
|
+
{
|
|
14
|
+
operations: StructuredDump.dump(operations),
|
|
15
|
+
profile_fingerprint: profile_fingerprint
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|