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,393 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Embed
|
|
7
|
+
module Contractable
|
|
8
|
+
class Runner
|
|
9
|
+
OutputsLike = Struct.new(:payload, keyword_init: true) do
|
|
10
|
+
def to_h
|
|
11
|
+
payload
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
ExecutionLike = Struct.new(:outputs, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
SEVERITY_MAP = {
|
|
17
|
+
observation: :info,
|
|
18
|
+
primary_success: :info,
|
|
19
|
+
candidate_success: :info,
|
|
20
|
+
divergence: :warning,
|
|
21
|
+
acceptance_failure: :warning,
|
|
22
|
+
primary_error: :error,
|
|
23
|
+
candidate_error: :error,
|
|
24
|
+
store_error: :error
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
SUMMARY_MAP = {
|
|
28
|
+
observation: "observation recorded",
|
|
29
|
+
primary_success: "primary succeeded",
|
|
30
|
+
candidate_success: "candidate succeeded",
|
|
31
|
+
divergence: "outputs diverged from primary",
|
|
32
|
+
acceptance_failure: "acceptance policy failed",
|
|
33
|
+
primary_error: "primary raised an error",
|
|
34
|
+
candidate_error: "candidate raised an error",
|
|
35
|
+
store_error: "store adapter raised an error"
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :config
|
|
39
|
+
|
|
40
|
+
def initialize(config:)
|
|
41
|
+
@config = config
|
|
42
|
+
@config.validate!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def call(*args, **kwargs)
|
|
46
|
+
observation_id = generate_observation_id
|
|
47
|
+
primary_result = primary_payload(args, kwargs, observation_id)
|
|
48
|
+
started_at = config.now
|
|
49
|
+
sampled = config.sampled?
|
|
50
|
+
dispatch_event(:primary_success, observation_id: observation_id, observation: nil, error: nil, metadata: { inputs: redacted_inputs(args, kwargs) })
|
|
51
|
+
|
|
52
|
+
if sampled
|
|
53
|
+
handoff = build_async_handoff(observation_id, args, kwargs)
|
|
54
|
+
work = -> { observe(observation_id: observation_id, started_at: started_at, primary_result: primary_result, args: args, kwargs: kwargs, sampled: true) }
|
|
55
|
+
dispatch_async(name: config.name, inputs: redacted_inputs(args, kwargs), metadata: metadata_payload, handoff: handoff, &work)
|
|
56
|
+
else
|
|
57
|
+
record_observation(sampled_observation(observation_id: observation_id, started_at: started_at, primary_result: primary_result, args: args, kwargs: kwargs))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
primary_result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def primary_payload(args, kwargs, observation_id)
|
|
66
|
+
invoke(config.primary_callable, args, kwargs)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
dispatch_event(:primary_error, observation_id: observation_id, observation: nil, error: serialize_error(e), metadata: { inputs: safe_redacted_inputs(args, kwargs) })
|
|
69
|
+
raise
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def observe(observation_id:, started_at:, primary_result:, args:, kwargs:, sampled:)
|
|
73
|
+
primary = normalize_side(config.primary_normalizer, primary_result)
|
|
74
|
+
candidate = candidate_payload(args, kwargs)
|
|
75
|
+
report = build_report(primary: primary, candidate: candidate, args: args, kwargs: kwargs)
|
|
76
|
+
acceptance = if candidate
|
|
77
|
+
Acceptance.evaluate(
|
|
78
|
+
policy: config.acceptance_policy,
|
|
79
|
+
report: report,
|
|
80
|
+
candidate: candidate,
|
|
81
|
+
options: config.acceptance_options
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
record_observation(
|
|
86
|
+
observation(
|
|
87
|
+
observation_id: observation_id,
|
|
88
|
+
started_at: started_at,
|
|
89
|
+
finished_at: config.now,
|
|
90
|
+
args: args,
|
|
91
|
+
kwargs: kwargs,
|
|
92
|
+
sampled: sampled,
|
|
93
|
+
primary: primary,
|
|
94
|
+
candidate: candidate,
|
|
95
|
+
report: report,
|
|
96
|
+
acceptance: acceptance
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def sampled_observation(observation_id:, started_at:, primary_result:, args:, kwargs:)
|
|
102
|
+
primary = normalize_side(config.primary_normalizer, primary_result)
|
|
103
|
+
observation(
|
|
104
|
+
observation_id: observation_id,
|
|
105
|
+
started_at: started_at,
|
|
106
|
+
finished_at: config.now,
|
|
107
|
+
args: args,
|
|
108
|
+
kwargs: kwargs,
|
|
109
|
+
sampled: false,
|
|
110
|
+
primary: primary,
|
|
111
|
+
candidate: nil,
|
|
112
|
+
report: nil,
|
|
113
|
+
acceptance: nil
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def candidate_payload(args, kwargs)
|
|
118
|
+
return nil if config.observed_service?
|
|
119
|
+
|
|
120
|
+
candidate_result = invoke(config.candidate_callable, args, kwargs)
|
|
121
|
+
normalize_side(config.candidate_normalizer, candidate_result)
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
{
|
|
124
|
+
status: :error,
|
|
125
|
+
outputs: {},
|
|
126
|
+
metadata: {},
|
|
127
|
+
error: serialize_error(e)
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def normalize_side(normalizer, value)
|
|
132
|
+
normalized = normalizer.call(value)
|
|
133
|
+
{
|
|
134
|
+
status: normalized.fetch(:status, :ok).to_sym,
|
|
135
|
+
outputs: normalize_hash(normalized.fetch(:outputs)),
|
|
136
|
+
metadata: normalize_hash(normalized.fetch(:metadata, {})),
|
|
137
|
+
error: normalized[:error]
|
|
138
|
+
}
|
|
139
|
+
rescue StandardError => e
|
|
140
|
+
{
|
|
141
|
+
status: :error,
|
|
142
|
+
outputs: {},
|
|
143
|
+
metadata: {},
|
|
144
|
+
error: serialize_error(e)
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def build_report(primary:, candidate:, args:, kwargs:)
|
|
149
|
+
return nil unless candidate
|
|
150
|
+
|
|
151
|
+
Igniter::Extensions::Contracts::DifferentialPack.compare(
|
|
152
|
+
inputs: redacted_inputs(args, kwargs),
|
|
153
|
+
primary_result: execution_like(primary.fetch(:outputs)),
|
|
154
|
+
candidate_result: execution_like(candidate.fetch(:outputs)),
|
|
155
|
+
primary_name: "#{config.name}:primary",
|
|
156
|
+
candidate_name: "#{config.name}:candidate"
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def observation(observation_id:, started_at:, finished_at:, args:, kwargs:, sampled:, primary:, candidate:, report:, acceptance:)
|
|
161
|
+
{
|
|
162
|
+
schema_version: 1,
|
|
163
|
+
receipt_kind: :contractable_observation,
|
|
164
|
+
observation_id: observation_id,
|
|
165
|
+
name: config.name,
|
|
166
|
+
role: config.role,
|
|
167
|
+
stage: config.stage,
|
|
168
|
+
mode: candidate ? :shadow : :observe,
|
|
169
|
+
async: config.async,
|
|
170
|
+
sampled: sampled,
|
|
171
|
+
status: nil,
|
|
172
|
+
started_at: serialize_time(started_at),
|
|
173
|
+
finished_at: serialize_time(finished_at),
|
|
174
|
+
duration_ms: duration_ms(started_at, finished_at),
|
|
175
|
+
inputs: redacted_inputs(args, kwargs),
|
|
176
|
+
primary: primary,
|
|
177
|
+
candidate: candidate,
|
|
178
|
+
report: report_payload(report),
|
|
179
|
+
match: report&.match?,
|
|
180
|
+
accepted: acceptance&.fetch(:accepted),
|
|
181
|
+
acceptance: acceptance,
|
|
182
|
+
error: candidate&.fetch(:error),
|
|
183
|
+
store_error: nil,
|
|
184
|
+
metadata: metadata_payload,
|
|
185
|
+
redaction: redaction_metadata
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def record_observation(observation)
|
|
190
|
+
if config.store_adapter
|
|
191
|
+
begin
|
|
192
|
+
observation[:status] = observation_status(observation)
|
|
193
|
+
if config.store_adapter.respond_to?(:record_observation)
|
|
194
|
+
config.store_adapter.record_observation(observation)
|
|
195
|
+
else
|
|
196
|
+
config.store_adapter.record(observation)
|
|
197
|
+
end
|
|
198
|
+
rescue StandardError => e
|
|
199
|
+
observation[:store_error] = serialize_error(e)
|
|
200
|
+
observation[:status] = observation_status(observation)
|
|
201
|
+
end
|
|
202
|
+
else
|
|
203
|
+
observation[:status] = observation_status(observation)
|
|
204
|
+
end
|
|
205
|
+
config.observation_callback&.call(observation)
|
|
206
|
+
dispatch_observation_events(observation)
|
|
207
|
+
observation
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def dispatch_observation_events(observation)
|
|
211
|
+
obs_id = observation[:observation_id]
|
|
212
|
+
dispatch_event(:candidate_success, observation_id: obs_id, observation: observation) if candidate_success?(observation)
|
|
213
|
+
dispatch_event(:candidate_error, observation_id: obs_id, observation: observation, error: observation[:error]) if observation[:error]
|
|
214
|
+
dispatch_event(:divergence, observation_id: obs_id, observation: observation) if observation[:match] == false
|
|
215
|
+
dispatch_event(:acceptance_failure, observation_id: obs_id, observation: observation) if observation[:accepted] == false
|
|
216
|
+
dispatch_event(:store_error, observation_id: obs_id, observation: observation, error: observation[:store_error]) if observation[:store_error]
|
|
217
|
+
dispatch_event(:observation, observation_id: obs_id, observation: observation)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def dispatch_event(event, observation_id:, observation:, error: nil, metadata: {})
|
|
221
|
+
receipt = build_event_receipt(event: event, observation_id: observation_id, observation: observation)
|
|
222
|
+
config.handlers_for(event).each do |event_handler|
|
|
223
|
+
event_handler.handler.call(
|
|
224
|
+
event_payload(
|
|
225
|
+
event: event,
|
|
226
|
+
observation: observation,
|
|
227
|
+
error: error,
|
|
228
|
+
metadata: metadata,
|
|
229
|
+
receipt: receipt
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
return unless config.store_adapter.respond_to?(:record_event)
|
|
234
|
+
|
|
235
|
+
begin
|
|
236
|
+
config.store_adapter.record_event(receipt)
|
|
237
|
+
rescue StandardError
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def event_payload(event:, observation:, error:, metadata:, receipt:)
|
|
243
|
+
{
|
|
244
|
+
name: config.name,
|
|
245
|
+
role: config.role,
|
|
246
|
+
stage: config.stage,
|
|
247
|
+
event: event,
|
|
248
|
+
observation: observation,
|
|
249
|
+
report: observation&.fetch(:report, nil),
|
|
250
|
+
error: error,
|
|
251
|
+
metadata: metadata_payload.merge(normalize_hash(metadata)),
|
|
252
|
+
receipt: receipt
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def build_event_receipt(event:, observation_id:, observation:)
|
|
257
|
+
observation_ref = if observation
|
|
258
|
+
{
|
|
259
|
+
observation_id: observation[:observation_id],
|
|
260
|
+
match: observation[:match],
|
|
261
|
+
accepted: observation[:accepted]
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
{
|
|
266
|
+
schema_version: 1,
|
|
267
|
+
receipt_kind: :contractable_event,
|
|
268
|
+
event_id: generate_event_id,
|
|
269
|
+
observation_id: observation_id,
|
|
270
|
+
event: event,
|
|
271
|
+
name: config.name,
|
|
272
|
+
occurred_at: serialize_time(config.now),
|
|
273
|
+
severity: SEVERITY_MAP.fetch(event, :info),
|
|
274
|
+
summary: SUMMARY_MAP.fetch(event, event.to_s.tr("_", " ")),
|
|
275
|
+
observation_ref: observation_ref,
|
|
276
|
+
metadata: {}
|
|
277
|
+
}
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def observation_status(observation)
|
|
281
|
+
return :unsampled if observation[:sampled] == false
|
|
282
|
+
return :store_error if observation[:store_error]
|
|
283
|
+
return :candidate_error if observation.dig(:candidate, :status) == :error
|
|
284
|
+
return :acceptance_failed if observation[:accepted] == false
|
|
285
|
+
return :diverged if observation[:match] == false
|
|
286
|
+
|
|
287
|
+
:ok
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def redaction_metadata
|
|
291
|
+
{
|
|
292
|
+
input_policy: config.redaction_input_policy,
|
|
293
|
+
output_policy: :none,
|
|
294
|
+
classes: []
|
|
295
|
+
}
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def build_async_handoff(observation_id, args, kwargs)
|
|
299
|
+
{
|
|
300
|
+
schema_version: 1,
|
|
301
|
+
kind: :contractable_async_handoff,
|
|
302
|
+
observation_id: observation_id,
|
|
303
|
+
name: config.name,
|
|
304
|
+
inputs: redacted_inputs(args, kwargs),
|
|
305
|
+
metadata: metadata_payload,
|
|
306
|
+
queued_at: serialize_time(config.now)
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def dispatch_async(name:, inputs:, metadata:, handoff:, &block)
|
|
311
|
+
adapter = config.async_adapter
|
|
312
|
+
params = adapter.method(:enqueue).parameters
|
|
313
|
+
accepts_handoff = params.any? { |(type, pname)| %i[key keyreq].include?(type) && pname == :handoff } ||
|
|
314
|
+
params.any? { |(type, _)| type == :keyrest }
|
|
315
|
+
if accepts_handoff
|
|
316
|
+
adapter.enqueue(name: name, inputs: inputs, metadata: metadata, handoff: handoff, &block)
|
|
317
|
+
else
|
|
318
|
+
adapter.enqueue(name: name, inputs: inputs, metadata: metadata, &block)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def candidate_success?(observation)
|
|
323
|
+
candidate = observation[:candidate]
|
|
324
|
+
candidate && candidate[:status] != :error
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def invoke(callable, args, kwargs)
|
|
328
|
+
return Igniter::Contracts::Contractable.invoke(callable, **kwargs).to_h if Igniter::Contracts::Contractable.contractable?(callable)
|
|
329
|
+
|
|
330
|
+
callable.call(*args, **kwargs)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def execution_like(outputs)
|
|
334
|
+
ExecutionLike.new(outputs: OutputsLike.new(payload: outputs))
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def report_payload(report)
|
|
338
|
+
return nil unless report
|
|
339
|
+
|
|
340
|
+
{
|
|
341
|
+
match: report.match?,
|
|
342
|
+
summary: report.summary,
|
|
343
|
+
details: report.to_h
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def redacted_inputs(args, kwargs)
|
|
348
|
+
normalize_hash(config.normalize_inputs(args, kwargs))
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def safe_redacted_inputs(args, kwargs)
|
|
352
|
+
redacted_inputs(args, kwargs)
|
|
353
|
+
rescue StandardError
|
|
354
|
+
{}
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def metadata_payload
|
|
358
|
+
normalize_hash(config.metadata_payload)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def normalize_hash(value)
|
|
362
|
+
value.to_h.transform_keys(&:to_sym)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def serialize_error(error)
|
|
366
|
+
{
|
|
367
|
+
type: error.class.name,
|
|
368
|
+
message: error.message,
|
|
369
|
+
details: error.respond_to?(:to_h) ? error.to_h : {}
|
|
370
|
+
}
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def serialize_time(value)
|
|
374
|
+
value.respond_to?(:iso8601) ? value.iso8601 : value.to_s
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def duration_ms(started_at, finished_at)
|
|
378
|
+
return nil unless started_at.respond_to?(:to_f) && finished_at.respond_to?(:to_f)
|
|
379
|
+
|
|
380
|
+
((finished_at.to_f - started_at.to_f) * 1000).round(3)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def generate_observation_id
|
|
384
|
+
"obs_#{SecureRandom.hex(12)}"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def generate_event_id
|
|
388
|
+
"evt_#{SecureRandom.hex(12)}"
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Embed
|
|
5
|
+
module Contractable
|
|
6
|
+
class SugarBuilder
|
|
7
|
+
attr_reader :configured
|
|
8
|
+
|
|
9
|
+
def initialize(config:)
|
|
10
|
+
@config = config
|
|
11
|
+
@configured = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def migration(from:, to:)
|
|
15
|
+
mark_configured!
|
|
16
|
+
config.role :migration_candidate
|
|
17
|
+
config.stage :shadowed
|
|
18
|
+
config.primary from
|
|
19
|
+
config.candidate to
|
|
20
|
+
config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def migrate(from, to:)
|
|
24
|
+
migration(from: from, to: to)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def observe(callable)
|
|
28
|
+
mark_configured!
|
|
29
|
+
config.role :observed_service
|
|
30
|
+
config.stage :captured
|
|
31
|
+
config.primary callable
|
|
32
|
+
config
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def discover(callable)
|
|
36
|
+
mark_configured!
|
|
37
|
+
config.role :discovery_probe
|
|
38
|
+
config.stage :profiled
|
|
39
|
+
config.primary callable
|
|
40
|
+
config
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def shadow(async: nil, sample: nil)
|
|
44
|
+
mark_configured!
|
|
45
|
+
config.async(async) unless async.nil?
|
|
46
|
+
config.sample(sample) unless sample.nil?
|
|
47
|
+
config
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def capture(**options)
|
|
51
|
+
mark_configured!
|
|
52
|
+
config.metadata(capture: options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def use(capability, adapter = nil, **options)
|
|
56
|
+
mark_configured!
|
|
57
|
+
case capability.to_sym
|
|
58
|
+
when :normalizer
|
|
59
|
+
require_adapter!(capability, adapter)
|
|
60
|
+
config.normalize_primary adapter
|
|
61
|
+
config.normalize_candidate adapter
|
|
62
|
+
when :redaction
|
|
63
|
+
config.redact_inputs adapter || redaction_adapter(**options)
|
|
64
|
+
config.redaction_input_policy = if adapter
|
|
65
|
+
:custom
|
|
66
|
+
elsif options[:only]
|
|
67
|
+
:only
|
|
68
|
+
else
|
|
69
|
+
:except
|
|
70
|
+
end
|
|
71
|
+
when :acceptance
|
|
72
|
+
raise SugarError, "use :acceptance requires policy:" unless options.key?(:policy)
|
|
73
|
+
|
|
74
|
+
policy = options.fetch(:policy)
|
|
75
|
+
config.accept policy, **options.reject { |key, _value| key == :policy }
|
|
76
|
+
when :store
|
|
77
|
+
require_adapter!(capability, adapter)
|
|
78
|
+
config.store adapter
|
|
79
|
+
when :logging, :reporting, :metrics, :validation
|
|
80
|
+
config.capability capability, explicit_capability_target(capability, adapter, **options)
|
|
81
|
+
else
|
|
82
|
+
raise SugarError, "use :#{capability} is not supported in this implementation slice"
|
|
83
|
+
end
|
|
84
|
+
config
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def on(event, callable = nil, &block)
|
|
88
|
+
mark_configured!
|
|
89
|
+
config.on(event, callable, &block)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def configured?
|
|
93
|
+
!!configured
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def method_missing(name, ...)
|
|
97
|
+
if config.respond_to?(name)
|
|
98
|
+
mark_configured!
|
|
99
|
+
return config.public_send(name, ...)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
super
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def respond_to_missing?(name, include_private = false)
|
|
106
|
+
config.respond_to?(name, include_private) || super
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
attr_reader :config
|
|
112
|
+
|
|
113
|
+
def mark_configured!
|
|
114
|
+
@configured = true
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def require_adapter!(capability, adapter)
|
|
118
|
+
return if adapter
|
|
119
|
+
|
|
120
|
+
raise SugarError, "use :#{capability} requires an explicit adapter"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def explicit_capability_target(capability, adapter = nil, contract: nil, callable: nil, target: nil)
|
|
124
|
+
targets = [adapter, contract, callable, target].compact
|
|
125
|
+
raise SugarError, "use :#{capability} requires an explicit target" if targets.empty?
|
|
126
|
+
raise SugarError, "use :#{capability} accepts only one explicit target" if targets.length > 1
|
|
127
|
+
|
|
128
|
+
targets.first
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def redaction_adapter(only: nil, except: nil)
|
|
132
|
+
raise SugarError, "use :redaction accepts only one of :only or :except" if only && except
|
|
133
|
+
raise SugarError, "use :redaction requires an adapter, :only, or :except" unless only || except
|
|
134
|
+
|
|
135
|
+
keys = Array(only || except).map(&:to_sym)
|
|
136
|
+
lambda do |*args, **kwargs|
|
|
137
|
+
inputs = kwargs.empty? && args.first.respond_to?(:to_h) ? args.first.to_h : kwargs
|
|
138
|
+
normalized = inputs.transform_keys(&:to_sym)
|
|
139
|
+
only ? normalized.slice(*keys) : normalized.reject { |key, _value| keys.include?(key) }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "contractable/acceptance"
|
|
4
|
+
require_relative "contractable/adapters"
|
|
5
|
+
require_relative "contractable/config"
|
|
6
|
+
require_relative "contractable/sugar_builder"
|
|
7
|
+
require_relative "contractable/runner"
|
|
8
|
+
|
|
9
|
+
module Igniter
|
|
10
|
+
module Embed
|
|
11
|
+
module Contractable
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def build(name, &block)
|
|
15
|
+
config = Config.new(name: name)
|
|
16
|
+
evaluate_block(config, &block) if block
|
|
17
|
+
Runner.new(config: config)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def evaluate_block(config, &block)
|
|
21
|
+
if block.arity.zero?
|
|
22
|
+
SugarBuilder.new(config: config).instance_eval(&block)
|
|
23
|
+
else
|
|
24
|
+
block.call(config)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Embed
|
|
5
|
+
class ContractsBuilder
|
|
6
|
+
def initialize(config:)
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def add(name_or_definition, definition = nil, as: nil, &block)
|
|
11
|
+
name, contract_definition = normalize_add_arguments(name_or_definition, definition, as: as)
|
|
12
|
+
config.contract(contract_definition, as: name)
|
|
13
|
+
build_contractable(name, contract_definition, &block) if block
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :config
|
|
20
|
+
|
|
21
|
+
def normalize_add_arguments(name_or_definition, definition, as:)
|
|
22
|
+
if ContractNaming.contract_class?(name_or_definition)
|
|
23
|
+
name = as && ContractNaming.normalize_contract_name(as)
|
|
24
|
+
return [name, name_or_definition]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
name = ContractNaming.normalize_contract_name(as || name_or_definition)
|
|
28
|
+
raise InvalidContractRegistrationError, "contract definition is required" unless definition
|
|
29
|
+
|
|
30
|
+
[name, definition]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_contractable(name, contract_definition, &block)
|
|
34
|
+
contractable_config = Contractable::Config.new(name: contractable_name(name, contract_definition))
|
|
35
|
+
builder = Contractable::SugarBuilder.new(config: contractable_config)
|
|
36
|
+
if block.arity.zero?
|
|
37
|
+
builder.instance_eval(&block)
|
|
38
|
+
else
|
|
39
|
+
block.call(builder)
|
|
40
|
+
end
|
|
41
|
+
return unless builder.configured?
|
|
42
|
+
|
|
43
|
+
config.contractable(contractable_config)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def contractable_name(name, contract_definition)
|
|
47
|
+
return name if name
|
|
48
|
+
return ContractNaming.infer_contract_name(contract_definition) if ContractNaming.contract_class?(contract_definition)
|
|
49
|
+
|
|
50
|
+
raise InvalidContractRegistrationError, "contractable name is required"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Embed
|
|
5
|
+
Error = Class.new(StandardError)
|
|
6
|
+
DiscoveryError = Class.new(Error)
|
|
7
|
+
DuplicateContractError = Class.new(Error)
|
|
8
|
+
InvalidContractRegistrationError = Class.new(Error)
|
|
9
|
+
SugarError = Class.new(Error)
|
|
10
|
+
UnknownContractError = Class.new(Error)
|
|
11
|
+
UnknownContractableError = Class.new(Error)
|
|
12
|
+
RailsIntegrationError = Class.new(Error)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Embed
|
|
5
|
+
class ExecutionEnvelope
|
|
6
|
+
attr_reader :name, :inputs, :result, :outputs, :errors, :metadata
|
|
7
|
+
|
|
8
|
+
def initialize(name:, inputs:, result: nil, errors: nil, metadata: {})
|
|
9
|
+
@name = name.to_sym
|
|
10
|
+
@inputs = inputs.freeze
|
|
11
|
+
@result = result
|
|
12
|
+
@outputs = result ? result.outputs : Igniter::Contracts::NamedValues.new({})
|
|
13
|
+
@errors = Array(errors).freeze
|
|
14
|
+
@metadata = metadata.freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def success?
|
|
19
|
+
result && errors.empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failure?
|
|
23
|
+
!success?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def output(name)
|
|
27
|
+
outputs.fetch(name.to_sym)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
name: name,
|
|
33
|
+
inputs: inputs,
|
|
34
|
+
success: success?,
|
|
35
|
+
outputs: outputs.to_h,
|
|
36
|
+
errors: errors.map { |error| normalize_error(error) },
|
|
37
|
+
metadata: metadata
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def normalize_error(error)
|
|
44
|
+
return error.to_h if error.respond_to?(:to_h)
|
|
45
|
+
|
|
46
|
+
{ class: error.class.name, message: error.message }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|