igniter-embed 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 +360 -0
- data/lib/igniter/embed/config.rb +155 -0
- data/lib/igniter/embed/container.rb +165 -0
- data/lib/igniter/embed/contract_handle.rb +23 -0
- data/lib/igniter/embed/contract_naming.rb +38 -0
- data/lib/igniter/embed/contractable/acceptance.rb +52 -0
- data/lib/igniter/embed/contractable/adapters.rb +33 -0
- data/lib/igniter/embed/contractable/config.rb +234 -0
- data/lib/igniter/embed/contractable/runner.rb +393 -0
- data/lib/igniter/embed/contractable/sugar_builder.rb +145 -0
- data/lib/igniter/embed/contractable.rb +29 -0
- data/lib/igniter/embed/contracts_builder.rb +54 -0
- data/lib/igniter/embed/errors.rb +14 -0
- data/lib/igniter/embed/execution_envelope.rb +50 -0
- data/lib/igniter/embed/host_builder.rb +39 -0
- data/lib/igniter/embed/rails.rb +21 -0
- data/lib/igniter/embed/registry.rb +78 -0
- data/lib/igniter/embed/sugar_expansion.rb +152 -0
- data/lib/igniter/embed.rb +38 -0
- data/lib/igniter-embed.rb +3 -0
- metadata +89 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Embed
|
|
5
|
+
module ContractNaming
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def contract_class?(value)
|
|
9
|
+
value.is_a?(Class) && value < Igniter::Contract
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def infer_contract_name(contract_class)
|
|
13
|
+
class_name = contract_class.name
|
|
14
|
+
unless class_name
|
|
15
|
+
raise InvalidContractRegistrationError,
|
|
16
|
+
"anonymous contract classes must be registered with as:"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
basename = class_name.split("::").last.sub(/Contract\z/, "")
|
|
20
|
+
snake_case(basename).to_sym
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def normalize_contract_name(name)
|
|
24
|
+
raise InvalidContractRegistrationError, "contract name is required" unless name
|
|
25
|
+
|
|
26
|
+
name.to_sym
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def snake_case(value)
|
|
30
|
+
value
|
|
31
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
32
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
33
|
+
.tr("-", "_")
|
|
34
|
+
.downcase
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Embed
|
|
5
|
+
module Contractable
|
|
6
|
+
module Acceptance
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def evaluate(policy:, report:, candidate:, options:)
|
|
10
|
+
case policy.to_sym
|
|
11
|
+
when :exact
|
|
12
|
+
result(policy: :exact, failures: report.match? ? [] : ["differential mismatch"])
|
|
13
|
+
when :completed
|
|
14
|
+
completed = candidate.fetch(:status) != :error && candidate.fetch(:error).nil?
|
|
15
|
+
result(policy: :completed, failures: completed ? [] : ["candidate did not complete"])
|
|
16
|
+
when :shape
|
|
17
|
+
failures = shape_failures(candidate.fetch(:outputs), options.fetch(:outputs, {}))
|
|
18
|
+
result(policy: :shape, failures: failures)
|
|
19
|
+
else
|
|
20
|
+
raise ArgumentError, "unknown contractable acceptance policy #{policy}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def result(policy:, failures:)
|
|
25
|
+
{
|
|
26
|
+
policy: policy,
|
|
27
|
+
accepted: failures.empty?,
|
|
28
|
+
failures: failures
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def shape_failures(outputs, expected, path = [])
|
|
33
|
+
expected.flat_map do |key, matcher|
|
|
34
|
+
current_path = path + [key]
|
|
35
|
+
next ["#{current_path.join(".")} is missing"] unless outputs.key?(key)
|
|
36
|
+
|
|
37
|
+
value = outputs.fetch(key)
|
|
38
|
+
if matcher.is_a?(Hash)
|
|
39
|
+
value.is_a?(Hash) ? shape_failures(value, matcher, current_path) : ["#{current_path.join(".")} is not a hash"]
|
|
40
|
+
elsif matcher.is_a?(Class) || matcher.is_a?(Module)
|
|
41
|
+
value.is_a?(matcher) ? [] : ["#{current_path.join(".")} is not a #{matcher}"]
|
|
42
|
+
elsif matcher.respond_to?(:call)
|
|
43
|
+
matcher.call(value) ? [] : ["#{current_path.join(".")} did not satisfy predicate"]
|
|
44
|
+
else
|
|
45
|
+
value == matcher ? [] : ["#{current_path.join(".")} did not equal #{matcher.inspect}"]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Embed
|
|
5
|
+
module Contractable
|
|
6
|
+
module Adapters
|
|
7
|
+
class InlineAsync
|
|
8
|
+
def enqueue(name:, inputs:, metadata:, handoff: nil, &block) # rubocop:disable Lint/UnusedMethodArgument
|
|
9
|
+
block.call
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class ThreadAsync
|
|
14
|
+
def enqueue(name:, inputs:, metadata:, handoff: nil, &block) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
+
Thread.new { block.call }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class MemoryStore
|
|
20
|
+
attr_reader :observations
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@observations = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def record(observation)
|
|
27
|
+
observations << observation
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Embed
|
|
5
|
+
module Contractable
|
|
6
|
+
class Config
|
|
7
|
+
EventHandler = Struct.new(:event, :handler, :source, keyword_init: true)
|
|
8
|
+
CapabilityAttachment = Struct.new(:name, :target, :kind, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
FAILURE_EVENTS = %i[primary_error candidate_error acceptance_failure store_error].freeze
|
|
11
|
+
SUPPORTED_EVENTS = (
|
|
12
|
+
%i[primary_success primary_error candidate_success candidate_error divergence acceptance_failure store_error observation] +
|
|
13
|
+
[:failure]
|
|
14
|
+
).freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :name, :event_handlers, :capability_attachments
|
|
17
|
+
attr_accessor :primary_callable, :candidate_callable,
|
|
18
|
+
:primary_normalizer, :candidate_normalizer,
|
|
19
|
+
:store_adapter, :observation_callback,
|
|
20
|
+
:acceptance_policy, :acceptance_options, :clock_callable,
|
|
21
|
+
:redaction_input_policy
|
|
22
|
+
|
|
23
|
+
def initialize(name:)
|
|
24
|
+
@name = name.to_sym
|
|
25
|
+
@role = nil
|
|
26
|
+
@stage = :captured
|
|
27
|
+
@stage_explicit = false
|
|
28
|
+
@async_enabled = true
|
|
29
|
+
@sample_value = 1.0
|
|
30
|
+
@async_adapter = nil
|
|
31
|
+
@metadata_value = {}
|
|
32
|
+
@input_redactor = ->(*, **) { {} }
|
|
33
|
+
@redaction_input_policy = :custom
|
|
34
|
+
@acceptance_policy = :exact
|
|
35
|
+
@acceptance_options = {}
|
|
36
|
+
@event_handlers = []
|
|
37
|
+
@capability_attachments = []
|
|
38
|
+
@clock_callable = Time
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def primary(callable = nil)
|
|
42
|
+
return primary_callable unless callable
|
|
43
|
+
|
|
44
|
+
self.primary_callable = callable
|
|
45
|
+
apply_core_contractable_defaults(callable)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def candidate(callable = nil)
|
|
49
|
+
return candidate_callable unless callable
|
|
50
|
+
|
|
51
|
+
self.candidate_callable = callable
|
|
52
|
+
apply_core_contractable_defaults(callable)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def normalize_primary(callable = nil, &block)
|
|
56
|
+
return primary_normalizer unless callable || block
|
|
57
|
+
|
|
58
|
+
self.primary_normalizer = callable || block
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def normalize_candidate(callable = nil, &block)
|
|
62
|
+
return candidate_normalizer unless callable || block
|
|
63
|
+
|
|
64
|
+
self.candidate_normalizer = callable || block
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def role(value = nil)
|
|
68
|
+
return @role || inferred_role unless value
|
|
69
|
+
|
|
70
|
+
@role = value.to_sym
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def stage(value = nil)
|
|
74
|
+
return @stage unless value
|
|
75
|
+
|
|
76
|
+
@stage = value.to_sym
|
|
77
|
+
@stage_explicit = true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def async(value = nil)
|
|
81
|
+
return @async_enabled if value.nil?
|
|
82
|
+
|
|
83
|
+
@async_enabled = !!value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def sample(value = nil)
|
|
87
|
+
return @sample_value if value.nil?
|
|
88
|
+
|
|
89
|
+
@sample_value = value
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def store(value = nil)
|
|
93
|
+
return store_adapter unless value
|
|
94
|
+
|
|
95
|
+
self.store_adapter = value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def async_adapter(value = nil)
|
|
99
|
+
return @async_adapter || default_async_adapter unless value
|
|
100
|
+
|
|
101
|
+
@async_adapter = value
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def redact_inputs(callable = nil, &block)
|
|
105
|
+
return @input_redactor unless callable || block
|
|
106
|
+
|
|
107
|
+
@input_redactor = callable || block
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def metadata(value = nil, &block)
|
|
111
|
+
return @metadata_value unless value || block
|
|
112
|
+
|
|
113
|
+
@metadata_value = block || value
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def on_observation(callable = nil, &block)
|
|
117
|
+
return observation_callback unless callable || block
|
|
118
|
+
|
|
119
|
+
self.observation_callback = callable || block
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def on(event, callable = nil, &block)
|
|
123
|
+
handler = callable || block
|
|
124
|
+
raise SugarError, "on :#{event} requires a block or callable" unless handler
|
|
125
|
+
|
|
126
|
+
normalized_event = event.to_sym
|
|
127
|
+
raise SugarError, "unsupported event :#{event}" unless SUPPORTED_EVENTS.include?(normalized_event)
|
|
128
|
+
|
|
129
|
+
event_names(normalized_event).each do |event_name|
|
|
130
|
+
event_handlers << EventHandler.new(event: event_name, handler: handler, source: normalized_event)
|
|
131
|
+
end
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def handlers_for(event)
|
|
136
|
+
event_handlers.select { |handler| handler.event == event.to_sym }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def capability(name, target)
|
|
140
|
+
capability_name = name.to_sym
|
|
141
|
+
raise SugarError, "capability :#{capability_name} is already configured" if capability_configured?(capability_name)
|
|
142
|
+
|
|
143
|
+
capability_attachments << CapabilityAttachment.new(
|
|
144
|
+
name: capability_name,
|
|
145
|
+
target: target,
|
|
146
|
+
kind: capability_kind(target)
|
|
147
|
+
)
|
|
148
|
+
self
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def accept(policy = nil, **options)
|
|
152
|
+
return acceptance_policy unless policy
|
|
153
|
+
|
|
154
|
+
self.acceptance_policy = policy.to_sym
|
|
155
|
+
self.acceptance_options = options
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate!
|
|
159
|
+
raise ArgumentError, "contractable #{name} requires a primary callable" unless primary_callable
|
|
160
|
+
raise ArgumentError, "contractable #{name} requires normalize_primary" unless primary_normalizer
|
|
161
|
+
|
|
162
|
+
return if observed_service?
|
|
163
|
+
raise ArgumentError, "contractable #{name} requires normalize_candidate when candidate is configured" unless candidate_normalizer
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def observed_service?
|
|
167
|
+
candidate_callable.nil?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def sampled?
|
|
171
|
+
value = sample_value
|
|
172
|
+
value = value.call if value.respond_to?(:call)
|
|
173
|
+
value.to_f >= 1.0 || rand < value.to_f
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def normalize_inputs(args, kwargs)
|
|
177
|
+
input_redactor.call(*args, **kwargs)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def metadata_payload
|
|
181
|
+
metadata_value.respond_to?(:call) ? metadata_value.call : metadata_value
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def now
|
|
185
|
+
clock_callable.respond_to?(:now) ? clock_callable.now : clock_callable.call
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
attr_reader :sample_value, :input_redactor, :metadata_value
|
|
191
|
+
|
|
192
|
+
def event_names(event)
|
|
193
|
+
event == :failure ? FAILURE_EVENTS : [event]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def capability_kind(target)
|
|
197
|
+
ContractNaming.contract_class?(target) ? :contract : :callable_adapter
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def capability_configured?(name)
|
|
201
|
+
capability_attachments.any? { |attachment| attachment.name == name }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def inferred_role
|
|
205
|
+
observed_service? ? :observed_service : :migration_candidate
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def default_async_adapter
|
|
209
|
+
async ? Adapters::ThreadAsync.new : Adapters::InlineAsync.new
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def apply_core_contractable_defaults(callable)
|
|
213
|
+
return unless Igniter::Contracts::Contractable.contractable?(callable)
|
|
214
|
+
|
|
215
|
+
definition = contractable_definition(callable)
|
|
216
|
+
@role ||= definition.role
|
|
217
|
+
@stage = definition.stage if definition.stage && !@stage_explicit
|
|
218
|
+
merge_core_contractable_metadata(definition.metadata)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def contractable_definition(callable)
|
|
222
|
+
klass = callable.is_a?(Class) ? callable : callable.class
|
|
223
|
+
klass.contractable_definition
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def merge_core_contractable_metadata(metadata)
|
|
227
|
+
return if metadata.empty? || !@metadata_value.is_a?(Hash)
|
|
228
|
+
|
|
229
|
+
@metadata_value = metadata.merge(@metadata_value)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|