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,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Contracts
|
|
7
|
+
module Assembly
|
|
8
|
+
class Profile
|
|
9
|
+
attr_reader :nodes,
|
|
10
|
+
:dsl_keywords,
|
|
11
|
+
:validators,
|
|
12
|
+
:normalizers,
|
|
13
|
+
:runtime_handlers,
|
|
14
|
+
:diagnostics_contributors,
|
|
15
|
+
:pack_manifests,
|
|
16
|
+
:effects,
|
|
17
|
+
:executors,
|
|
18
|
+
:fingerprint
|
|
19
|
+
|
|
20
|
+
def self.build_from(kernel)
|
|
21
|
+
payload = {
|
|
22
|
+
nodes: kernel.nodes.to_h.freeze,
|
|
23
|
+
dsl_keywords: kernel.dsl_keywords.to_h.freeze,
|
|
24
|
+
validators: kernel.validators.entries.freeze,
|
|
25
|
+
normalizers: kernel.normalizers.entries.freeze,
|
|
26
|
+
runtime_handlers: kernel.runtime_handlers.to_h.freeze,
|
|
27
|
+
diagnostics_contributors: kernel.diagnostics_contributors.entries.freeze,
|
|
28
|
+
pack_manifests: kernel.pack_manifests.dup.freeze,
|
|
29
|
+
effects: kernel.effects.to_h.freeze,
|
|
30
|
+
executors: kernel.executors.to_h.freeze
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
new(**payload, fingerprint: fingerprint_for(payload))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.fingerprint_for(payload)
|
|
37
|
+
normalized = payload.map do |key, value|
|
|
38
|
+
serialized = serialize_for_fingerprint(value)
|
|
39
|
+
[key.to_s, serialized]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Digest::SHA256.hexdigest(normalized.inspect)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.serialize_for_fingerprint(value)
|
|
46
|
+
case value
|
|
47
|
+
when Hash
|
|
48
|
+
value.map { |entry_key, entry_value| [entry_key.to_s, entry_value.inspect] }
|
|
49
|
+
when Array
|
|
50
|
+
value.map do |entry|
|
|
51
|
+
if entry.respond_to?(:key) && entry.respond_to?(:value)
|
|
52
|
+
[entry.key.to_s, entry.value.inspect]
|
|
53
|
+
else
|
|
54
|
+
entry.inspect
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
value.inspect
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(nodes:, dsl_keywords:, validators:, normalizers:, runtime_handlers:, diagnostics_contributors:,
|
|
63
|
+
pack_manifests:, effects:, executors:, fingerprint:)
|
|
64
|
+
@nodes = nodes
|
|
65
|
+
@dsl_keywords = dsl_keywords
|
|
66
|
+
@validators = validators
|
|
67
|
+
@normalizers = normalizers
|
|
68
|
+
@runtime_handlers = runtime_handlers
|
|
69
|
+
@diagnostics_contributors = diagnostics_contributors
|
|
70
|
+
@pack_manifests = pack_manifests
|
|
71
|
+
@effects = effects
|
|
72
|
+
@executors = executors
|
|
73
|
+
@fingerprint = fingerprint
|
|
74
|
+
freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def node_class(kind)
|
|
78
|
+
nodes.fetch(kind.to_sym)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def dsl_keyword(name)
|
|
82
|
+
dsl_keywords.fetch(name.to_sym)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def runtime_handler(kind)
|
|
86
|
+
runtime_handlers.fetch(kind.to_sym)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def effect(name)
|
|
90
|
+
effects.fetch(name.to_sym)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def executor(name)
|
|
94
|
+
executors.fetch(name.to_sym)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def supports_node_kind?(kind)
|
|
98
|
+
nodes.key?(kind.to_sym)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def supports_effect?(name)
|
|
102
|
+
effects.key?(name.to_sym)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def supports_executor?(name)
|
|
106
|
+
executors.key?(name.to_sym)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def pack_names
|
|
110
|
+
pack_manifests.map(&:name)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def pack_manifest(name)
|
|
114
|
+
pack_manifests.find { |manifest| manifest.name == name.to_sym }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def provided_capabilities
|
|
118
|
+
pack_manifests.flat_map(&:provides_capabilities).uniq
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def required_capabilities
|
|
122
|
+
pack_manifests.flat_map(&:requires_capabilities).uniq
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def declared_registry_keys(registry)
|
|
126
|
+
pack_manifests
|
|
127
|
+
.flat_map { |manifest| manifest.declared_keys_for(registry) }
|
|
128
|
+
.uniq
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Assembly
|
|
6
|
+
module ProjectPack
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def manifest
|
|
10
|
+
PackManifest.new(
|
|
11
|
+
name: :project,
|
|
12
|
+
registry_contracts: [PackManifest.dsl_keyword(:project)]
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def install_into(kernel)
|
|
17
|
+
kernel.dsl_keywords.register(:project, DslKeyword.new(:project) do |name, from:, builder:, key: nil, dig: nil, default: PathAccess::NO_DEFAULT|
|
|
18
|
+
source_name = from.to_sym
|
|
19
|
+
path = PathAccess.normalize_path(keyword_name: :project, key: key, dig: dig)
|
|
20
|
+
|
|
21
|
+
builder.add_operation(
|
|
22
|
+
kind: :compute,
|
|
23
|
+
name: name,
|
|
24
|
+
depends_on: [source_name],
|
|
25
|
+
callable: lambda do |**values|
|
|
26
|
+
source = values.fetch(source_name)
|
|
27
|
+
PathAccess.fetch_path(
|
|
28
|
+
source,
|
|
29
|
+
path,
|
|
30
|
+
source_name: source_name,
|
|
31
|
+
keyword_name: :project,
|
|
32
|
+
default: default
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
)
|
|
36
|
+
end)
|
|
37
|
+
kernel
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Assembly
|
|
6
|
+
class Registry
|
|
7
|
+
def initialize(name:)
|
|
8
|
+
@name = name
|
|
9
|
+
@entries = {}
|
|
10
|
+
@frozen = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register(key, value)
|
|
14
|
+
raise FrozenRegistryError, "#{@name} is frozen" if frozen?
|
|
15
|
+
|
|
16
|
+
normalized_key = normalize_key(key)
|
|
17
|
+
raise DuplicateRegistrationError, "#{@name} already has #{normalized_key}" if @entries.key?(normalized_key)
|
|
18
|
+
|
|
19
|
+
@entries[normalized_key] = value
|
|
20
|
+
value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fetch(key)
|
|
24
|
+
@entries.fetch(normalize_key(key))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def registered?(key)
|
|
28
|
+
@entries.key?(normalize_key(key))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def entries
|
|
32
|
+
@entries.dup
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
entries
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def freeze!
|
|
40
|
+
@frozen = true
|
|
41
|
+
@entries.freeze
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def frozen?
|
|
46
|
+
@frozen
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def normalize_key(key)
|
|
52
|
+
key.to_sym
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Contracts
|
|
5
|
+
module Assembly
|
|
6
|
+
module StepResultPack
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def manifest
|
|
10
|
+
PackManifest.new(
|
|
11
|
+
name: :step_result,
|
|
12
|
+
node_contracts: [PackManifest.node(:step)],
|
|
13
|
+
registry_contracts: [
|
|
14
|
+
PackManifest.validator(:step_dependencies),
|
|
15
|
+
PackManifest.validator(:step_callables),
|
|
16
|
+
PackManifest.diagnostic(:step_trace)
|
|
17
|
+
]
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def install_into(kernel)
|
|
22
|
+
kernel.nodes.register(:step, NodeType.new(kind: :step, metadata: { category: :pipeline }))
|
|
23
|
+
kernel.dsl_keywords.register(:step, step_keyword)
|
|
24
|
+
kernel.validators.register(:step_dependencies, Execution::StepResultValidators.method(:validate_step_dependencies))
|
|
25
|
+
kernel.validators.register(:step_callables, Execution::StepResultValidators.method(:validate_step_callables))
|
|
26
|
+
kernel.runtime_handlers.register(:step, Execution::StepResultRuntime.method(:handle_step))
|
|
27
|
+
kernel.diagnostics_contributors.register(:step_trace, Execution::StepResultDiagnostics)
|
|
28
|
+
kernel
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def step_keyword
|
|
32
|
+
DslKeyword.new(:step, lambda { |name, builder:, **attributes, &block|
|
|
33
|
+
normalized_attributes = attributes.dup
|
|
34
|
+
normalized_attributes[:callable] = normalized_attributes.delete(:call) if normalized_attributes.key?(:call)
|
|
35
|
+
normalized_attributes[:callable] = block if block
|
|
36
|
+
builder.add_operation(kind: :step, name: name, **normalized_attributes)
|
|
37
|
+
})
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "assembly/registry"
|
|
4
|
+
require_relative "assembly/ordered_registry"
|
|
5
|
+
require_relative "assembly/pack"
|
|
6
|
+
require_relative "assembly/pack_manifest"
|
|
7
|
+
require_relative "assembly/node_type"
|
|
8
|
+
require_relative "assembly/dsl_keyword"
|
|
9
|
+
require_relative "assembly/path_access"
|
|
10
|
+
require_relative "assembly/hook_result_policies"
|
|
11
|
+
require_relative "assembly/hook_spec"
|
|
12
|
+
require_relative "assembly/hook_specs"
|
|
13
|
+
require_relative "assembly/profile"
|
|
14
|
+
require_relative "assembly/kernel"
|
|
15
|
+
require_relative "assembly/baseline_pack"
|
|
16
|
+
require_relative "assembly/const_pack"
|
|
17
|
+
require_relative "assembly/project_pack"
|
|
18
|
+
require_relative "assembly/step_result_pack"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
class Contract
|
|
5
|
+
class ResultReader
|
|
6
|
+
def initialize(outputs)
|
|
7
|
+
@outputs = outputs
|
|
8
|
+
freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def output(name)
|
|
12
|
+
@outputs.fetch(name.to_sym)
|
|
13
|
+
rescue KeyError
|
|
14
|
+
raise KeyError, "unknown contract output #{name}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
@outputs.to_h
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def method_missing(name, *args, &block)
|
|
22
|
+
return output(name) if args.empty? && block.nil? && @outputs.key?(name)
|
|
23
|
+
raise KeyError, "unknown contract output #{name}" if args.empty? && block.nil?
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def respond_to_missing?(name, include_private = false)
|
|
29
|
+
@outputs.key?(name) || super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
attr_writer :profile
|
|
35
|
+
|
|
36
|
+
def inherited(subclass)
|
|
37
|
+
super
|
|
38
|
+
subclass.profile = profile
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def define(&block)
|
|
42
|
+
raise ArgumentError, "contract definition requires a block" unless block
|
|
43
|
+
|
|
44
|
+
@definition_block = block
|
|
45
|
+
@compiled_graphs = {}
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def definition_block
|
|
50
|
+
@definition_block || raise(Contracts::Error, "#{name || self} does not define a contract")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def profile
|
|
54
|
+
@profile || Contracts.default_profile
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def compile(profile: self.profile)
|
|
58
|
+
compiled_graphs.fetch(profile.fingerprint) do
|
|
59
|
+
compiled_graphs[profile.fingerprint] = Contracts.compile(profile: profile, &definition_block)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def call(inputs = nil, profile: self.profile, **keyword_inputs)
|
|
64
|
+
new(inputs, profile: profile, **keyword_inputs)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def execute(inputs = nil, profile: self.profile, **keyword_inputs)
|
|
68
|
+
contract = new(inputs, profile: profile, **keyword_inputs)
|
|
69
|
+
contract.execution_result
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def compiled_graphs
|
|
75
|
+
@compiled_graphs ||= {}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
attr_reader :inputs, :profile, :execution_result
|
|
80
|
+
|
|
81
|
+
def initialize(inputs = nil, profile: self.class.profile, **keyword_inputs)
|
|
82
|
+
@profile = profile
|
|
83
|
+
@inputs = normalize_inputs(inputs, keyword_inputs)
|
|
84
|
+
run!
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def result
|
|
88
|
+
ResultReader.new(execution_result.outputs)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def outputs
|
|
92
|
+
execution_result.outputs
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def output(name)
|
|
96
|
+
result.output(name)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def success?
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def failure?
|
|
104
|
+
!success?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def update_inputs(inputs = nil, **keyword_inputs)
|
|
108
|
+
@inputs = @inputs.merge(normalize_inputs(inputs, keyword_inputs))
|
|
109
|
+
run!
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def to_h
|
|
114
|
+
{
|
|
115
|
+
contract: self.class.name,
|
|
116
|
+
inputs: inputs.dup,
|
|
117
|
+
outputs: outputs.to_h,
|
|
118
|
+
success: success?
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def run!
|
|
125
|
+
@execution_result = Contracts.execute(self.class.compile(profile: profile), inputs: inputs, profile: profile)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def normalize_inputs(inputs, keyword_inputs)
|
|
129
|
+
normalized_inputs = {}
|
|
130
|
+
normalized_inputs.merge!(inputs) if inputs
|
|
131
|
+
normalized_inputs.merge!(keyword_inputs)
|
|
132
|
+
normalized_inputs.transform_keys(&:to_sym)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|