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.
@@ -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