smplkit 3.0.96 → 3.0.97
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 +4 -4
- data/lib/smplkit/account/client.rb +9 -2
- data/lib/smplkit/account/models.rb +18 -0
- data/lib/smplkit/api_support.rb +8 -0
- data/lib/smplkit/audit/buffer.rb +3 -1
- data/lib/smplkit/audit/categories.rb +21 -10
- data/lib/smplkit/audit/client.rb +16 -6
- data/lib/smplkit/audit/event_types.rb +26 -10
- data/lib/smplkit/audit/events.rb +93 -17
- data/lib/smplkit/audit/forwarders.rb +27 -16
- data/lib/smplkit/audit/models.rb +46 -31
- data/lib/smplkit/audit/resource_types.rb +21 -9
- data/lib/smplkit/buffers.rb +15 -0
- data/lib/smplkit/client.rb +38 -6
- data/lib/smplkit/config/client.rb +279 -26
- data/lib/smplkit/config/helpers.rb +44 -6
- data/lib/smplkit/config/models.rb +114 -7
- data/lib/smplkit/config_resolution.rb +6 -4
- data/lib/smplkit/errors.rb +6 -3
- data/lib/smplkit/flags/client.rb +135 -7
- data/lib/smplkit/flags/models.rb +110 -8
- data/lib/smplkit/flags/types.rb +2 -2
- data/lib/smplkit/jobs/client.rb +47 -26
- data/lib/smplkit/jobs/models.rb +47 -18
- data/lib/smplkit/logging/client.rb +125 -16
- data/lib/smplkit/logging/helpers.rb +4 -1
- data/lib/smplkit/logging/levels.rb +3 -1
- data/lib/smplkit/logging/models.rb +71 -5
- data/lib/smplkit/logging/normalize.rb +3 -1
- data/lib/smplkit/logging/resolution.rb +4 -4
- data/lib/smplkit/platform/client.rb +132 -7
- data/lib/smplkit/platform/models.rb +103 -3
- data/lib/smplkit/platform/types.rb +14 -0
- data/lib/smplkit/transport.rb +5 -1
- data/lib/smplkit/ws.rb +1 -1
- metadata +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Smpl Config has two surfaces on a single client, mirroring how the audit and
|
|
6
6
|
# jobs clients expose their full surface from one class:
|
|
7
7
|
#
|
|
8
|
-
# * *
|
|
8
|
+
# * *CRUD surface* — pure CRUD, no live connection: +new+ / +get+ /
|
|
9
9
|
# +list+ / +delete+ and the discovery buffer (+register_config+ /
|
|
10
10
|
# +register_config_item+ / +flush+ / +pending_count+). The client owns the
|
|
11
11
|
# discovery buffer directly.
|
|
@@ -29,6 +29,8 @@ module Smplkit
|
|
|
29
29
|
module Config
|
|
30
30
|
# Module-level helpers for the config client. Extracted so they can be
|
|
31
31
|
# unit-tested without spinning up the full client.
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
32
34
|
module Discovery
|
|
33
35
|
module_function
|
|
34
36
|
|
|
@@ -36,6 +38,10 @@ module Smplkit
|
|
|
36
38
|
# Hash/Struct target and when supplying a default to +get_value(id, key,
|
|
37
39
|
# default)+. +true+/+false+ are checked first because Ruby's
|
|
38
40
|
# +Numeric+/+Integer+ tests would not accidentally claim them.
|
|
41
|
+
#
|
|
42
|
+
# @api private
|
|
43
|
+
# @param value [Object] The runtime value whose item type to infer.
|
|
44
|
+
# @return [String] One of +"BOOLEAN"+, +"NUMBER"+, or +"STRING"+.
|
|
39
45
|
def value_to_item_type(value)
|
|
40
46
|
case value
|
|
41
47
|
when true, false then "BOOLEAN"
|
|
@@ -47,6 +53,12 @@ module Smplkit
|
|
|
47
53
|
# Walk a bound target, returning +[key, type, value, description]+ tuples
|
|
48
54
|
# flattened to dot-notation. Nested Hashes / Structs are descended into;
|
|
49
55
|
# everything else is treated as an opaque leaf.
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
# @param target [Hash, Struct] The bound target to walk.
|
|
59
|
+
# @param prefix [String] Dot-notation prefix accumulated during recursion.
|
|
60
|
+
# @return [Array<Array(String, String, Object, String, nil)>] One
|
|
61
|
+
# +[key, type, value, description]+ tuple per leaf.
|
|
50
62
|
def iter_items(target, prefix: "")
|
|
51
63
|
if target.is_a?(Hash)
|
|
52
64
|
iter_hash_items(target, prefix: prefix)
|
|
@@ -57,6 +69,14 @@ module Smplkit
|
|
|
57
69
|
end
|
|
58
70
|
end
|
|
59
71
|
|
|
72
|
+
# Walk a Hash leaf-by-leaf, flattening nested Hashes / Structs to
|
|
73
|
+
# dot-notation.
|
|
74
|
+
#
|
|
75
|
+
# @api private
|
|
76
|
+
# @param hash [Hash] The Hash to walk.
|
|
77
|
+
# @param prefix [String] Dot-notation prefix accumulated during recursion.
|
|
78
|
+
# @return [Array<Array(String, String, Object, String, nil)>] One
|
|
79
|
+
# +[key, type, value, description]+ tuple per leaf.
|
|
60
80
|
def iter_hash_items(hash, prefix: "")
|
|
61
81
|
out = []
|
|
62
82
|
hash.each do |raw_key, value|
|
|
@@ -70,6 +90,14 @@ module Smplkit
|
|
|
70
90
|
out
|
|
71
91
|
end
|
|
72
92
|
|
|
93
|
+
# Walk a Struct member-by-member, flattening nested Hashes / Structs to
|
|
94
|
+
# dot-notation.
|
|
95
|
+
#
|
|
96
|
+
# @api private
|
|
97
|
+
# @param struct [Struct] The Struct to walk.
|
|
98
|
+
# @param prefix [String] Dot-notation prefix accumulated during recursion.
|
|
99
|
+
# @return [Array<Array(String, String, Object, String, nil)>] One
|
|
100
|
+
# +[key, type, value, description]+ tuple per leaf.
|
|
73
101
|
def iter_struct_items(struct, prefix: "")
|
|
74
102
|
out = []
|
|
75
103
|
struct.members.each do |member|
|
|
@@ -89,6 +117,12 @@ module Smplkit
|
|
|
89
117
|
# +Hash#[]=+ or +Struct#[]=+. Bails silently if any intermediate is
|
|
90
118
|
# missing or not a supported container — the server may have items that
|
|
91
119
|
# don't line up with what the bound target declared.
|
|
120
|
+
#
|
|
121
|
+
# @api private
|
|
122
|
+
# @param target [Hash, Struct] The bound target to mutate in place.
|
|
123
|
+
# @param dotted_key [String] The dot-notation path to the leaf to set.
|
|
124
|
+
# @param value [Object] The value to assign at the leaf.
|
|
125
|
+
# @return [void]
|
|
92
126
|
def apply_change_to_target(target, dotted_key, value)
|
|
93
127
|
parts = dotted_key.split(".")
|
|
94
128
|
current = walk_to_leaf_parent(target, parts[0..-2])
|
|
@@ -102,6 +136,14 @@ module Smplkit
|
|
|
102
136
|
end
|
|
103
137
|
end
|
|
104
138
|
|
|
139
|
+
# Walk a dotted key path to the leaf's parent container.
|
|
140
|
+
#
|
|
141
|
+
# @api private
|
|
142
|
+
# @param target [Hash, Struct] The root container to walk from.
|
|
143
|
+
# @param parts [Array<String>] Path segments leading to (but excluding)
|
|
144
|
+
# the leaf.
|
|
145
|
+
# @return [Hash, Struct, nil] The leaf's parent container, or +nil+ when
|
|
146
|
+
# any intermediate is missing or not a supported container.
|
|
105
147
|
def walk_to_leaf_parent(target, parts)
|
|
106
148
|
current = target
|
|
107
149
|
parts.each do |part|
|
|
@@ -122,6 +164,14 @@ module Smplkit
|
|
|
122
164
|
current
|
|
123
165
|
end
|
|
124
166
|
|
|
167
|
+
# Assign a value to a Struct member, ignoring members the Struct does not
|
|
168
|
+
# declare.
|
|
169
|
+
#
|
|
170
|
+
# @api private
|
|
171
|
+
# @param struct [Struct] The Struct to mutate.
|
|
172
|
+
# @param name [String] The member name to assign.
|
|
173
|
+
# @param value [Object] The value to assign.
|
|
174
|
+
# @return [void]
|
|
125
175
|
def assign_struct_member(struct, name, value)
|
|
126
176
|
sym = name.to_sym
|
|
127
177
|
return unless struct.members.include?(sym)
|
|
@@ -179,27 +229,60 @@ module Smplkit
|
|
|
179
229
|
|
|
180
230
|
attr_reader :config_id
|
|
181
231
|
|
|
232
|
+
# @return [Array<String>] The current resolved item keys.
|
|
182
233
|
def keys = current_values.keys
|
|
234
|
+
|
|
235
|
+
# @return [Array<Object>] The current resolved values.
|
|
183
236
|
def values = current_values.values
|
|
237
|
+
|
|
238
|
+
# @yieldparam key [String] Each resolved item key.
|
|
239
|
+
# @yieldparam value [Object] Each resolved value.
|
|
240
|
+
# @return [Enumerator, void] An enumerator when no block is given.
|
|
184
241
|
def each_pair(&) = current_values.each_pair(&)
|
|
185
242
|
alias each each_pair
|
|
243
|
+
|
|
244
|
+
# @return [Array<Array(String, Object)>] The current resolved items as
|
|
245
|
+
# +[key, value]+ pairs.
|
|
186
246
|
def items = current_values.to_a
|
|
247
|
+
|
|
248
|
+
# @return [Hash{String => Object}] A copy of the current resolved values.
|
|
187
249
|
def to_h = current_values.dup
|
|
250
|
+
|
|
251
|
+
# @return [Integer] The number of resolved items.
|
|
188
252
|
def size = current_values.size
|
|
189
253
|
alias length size
|
|
254
|
+
|
|
255
|
+
# @param key [String, Symbol] The item key to test for.
|
|
256
|
+
# @return [Boolean] +true+ when +key+ is present in the resolved values.
|
|
190
257
|
def key?(key) = current_values.key?(key.to_s)
|
|
191
258
|
alias include? key?
|
|
192
259
|
alias has_key? key?
|
|
193
260
|
|
|
261
|
+
# @param key [String, Symbol] The item key to read.
|
|
262
|
+
# @return [Object, nil] The current resolved value for +key+, or +nil+
|
|
263
|
+
# when absent.
|
|
194
264
|
def [](key)
|
|
195
265
|
current_values[key.to_s]
|
|
196
266
|
end
|
|
197
267
|
|
|
268
|
+
# Return the current resolved value for +key+, or a fallback.
|
|
269
|
+
#
|
|
270
|
+
# @param key [String, Symbol] The config item key to read.
|
|
271
|
+
# @param default [Object] Value returned when +key+ is not present.
|
|
272
|
+
# @return [Object] The current resolved value for +key+, or +default+ if
|
|
273
|
+
# the key is absent.
|
|
198
274
|
def get(key, default = nil)
|
|
199
275
|
values = current_values
|
|
200
276
|
values.key?(key.to_s) ? values[key.to_s] : default
|
|
201
277
|
end
|
|
202
278
|
|
|
279
|
+
# Register a change listener scoped to this config.
|
|
280
|
+
#
|
|
281
|
+
# @param item_key [String, Symbol, nil] When given, fires only when this
|
|
282
|
+
# item key changes; otherwise fires on any change to this config.
|
|
283
|
+
# @yieldparam event [ConfigChangeEvent] The change that fired the
|
|
284
|
+
# listener.
|
|
285
|
+
# @return [Proc] The registered block, unchanged.
|
|
203
286
|
def on_change(item_key = nil, &)
|
|
204
287
|
if item_key.nil?
|
|
205
288
|
@client.on_change(@config_id, &)
|
|
@@ -233,6 +316,13 @@ module Smplkit
|
|
|
233
316
|
end
|
|
234
317
|
|
|
235
318
|
# Normalize a +parent+ argument to a config id string.
|
|
319
|
+
#
|
|
320
|
+
# @api private
|
|
321
|
+
# @param parent [String, Config, nil] A config id, a saved +Config+ whose
|
|
322
|
+
# id is used, or +nil+.
|
|
323
|
+
# @return [String, nil] The resolved config id, or +nil+ when +parent+ is
|
|
324
|
+
# +nil+.
|
|
325
|
+
# @raise [ArgumentError] If +parent+ is an unsaved +Config+ (no id yet).
|
|
236
326
|
def self.resolve_parent_id(parent)
|
|
237
327
|
return parent if parent.nil? || parent.is_a?(String)
|
|
238
328
|
if parent.id.nil? || parent.id == ""
|
|
@@ -245,19 +335,32 @@ module Smplkit
|
|
|
245
335
|
# Build a standalone config transport and resolve the app base URL.
|
|
246
336
|
#
|
|
247
337
|
# +base_url+/+api_key+ are used directly when supplied (the path a top-level
|
|
248
|
-
# client takes after it has already resolved them); otherwise the
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
#
|
|
338
|
+
# client takes after it has already resolved them); otherwise the config
|
|
339
|
+
# resolver fills in whatever is missing (+~/.smplkit+ / env vars /
|
|
340
|
+
# defaults). The app base URL is returned alongside so a standalone client
|
|
341
|
+
# can open its own WebSocket against the event gateway.
|
|
342
|
+
#
|
|
343
|
+
# @api private
|
|
344
|
+
# @param api_key [String, nil] API key, or +nil+ to resolve it.
|
|
345
|
+
# @param base_url [String, nil] Full config-service base URL, or +nil+ to
|
|
346
|
+
# resolve it from +base_domain+/+scheme+.
|
|
347
|
+
# @param profile [String, nil] Named +~/.smplkit+ profile section.
|
|
348
|
+
# @param base_domain [String, nil] Base domain for API requests.
|
|
349
|
+
# @param scheme [String, nil] URL scheme.
|
|
350
|
+
# @param debug [Boolean, nil] Enable SDK debug logging.
|
|
351
|
+
# @param extra_headers [Hash{String => String}, nil] Headers attached to
|
|
352
|
+
# every request.
|
|
353
|
+
# @return [Array(Object, String, String)] The transport, the app base URL,
|
|
354
|
+
# and the resolved API key.
|
|
252
355
|
def self.config_transport(api_key:, base_url:, profile:, base_domain:, scheme:, debug:, extra_headers:)
|
|
253
|
-
cfg = ConfigResolution.
|
|
356
|
+
cfg = ConfigResolution.resolve_client_config(
|
|
254
357
|
profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme, debug: debug
|
|
255
358
|
)
|
|
256
359
|
resolved_key = api_key.nil? ? cfg.api_key : api_key
|
|
257
360
|
merged = {}
|
|
258
361
|
merged.merge!(cfg.extra_headers || {})
|
|
259
362
|
merged.merge!(extra_headers || {})
|
|
260
|
-
tcfg = ConfigResolution::
|
|
363
|
+
tcfg = ConfigResolution::ResolvedClientConfig.new(
|
|
261
364
|
api_key: resolved_key, base_domain: cfg.base_domain, scheme: cfg.scheme,
|
|
262
365
|
debug: cfg.debug, extra_headers: merged
|
|
263
366
|
)
|
|
@@ -278,7 +381,7 @@ module Smplkit
|
|
|
278
381
|
# proxy = config.subscribe("billing")
|
|
279
382
|
# puts proxy["max_seats"]
|
|
280
383
|
#
|
|
281
|
-
# The
|
|
384
|
+
# The CRUD surface (+new+ / +get+ / +list+ / +delete+ and discovery)
|
|
282
385
|
# is pure CRUD. The live surface (+subscribe+ / +get_value+ / +bind+ /
|
|
283
386
|
# +on_change+ / +refresh+) connects lazily on first use — the first call
|
|
284
387
|
# flushes discovery, fetches and resolves all configs into the local cache,
|
|
@@ -290,6 +393,26 @@ module Smplkit
|
|
|
290
393
|
MISSING = Object.new.freeze
|
|
291
394
|
private_constant :MISSING
|
|
292
395
|
|
|
396
|
+
# @param api_key [String, nil] API key. When omitted, resolved from
|
|
397
|
+
# +SMPLKIT_API_KEY+ or +~/.smplkit+.
|
|
398
|
+
# @param environment [String, nil] Deployment environment used to resolve
|
|
399
|
+
# runtime config values and to scope discovery declarations.
|
|
400
|
+
# @param base_url [String, nil] Full config-service base URL. Usually
|
|
401
|
+
# resolved from +base_domain+/+scheme+; supplied directly by the
|
|
402
|
+
# top-level clients which have already computed it.
|
|
403
|
+
# @param profile [String, nil] Named +~/.smplkit+ profile section.
|
|
404
|
+
# @param base_domain [String, nil] Base domain for API requests (default
|
|
405
|
+
# +"smplkit.com"+).
|
|
406
|
+
# @param scheme [String, nil] URL scheme (default +"https"+).
|
|
407
|
+
# @param debug [Boolean, nil] Enable SDK debug logging.
|
|
408
|
+
# @param extra_headers [Hash{String => String}, nil] Extra headers
|
|
409
|
+
# attached to every request.
|
|
410
|
+
# @param parent [Smplkit::Client, nil] Internal — the owning client. Not
|
|
411
|
+
# for direct use.
|
|
412
|
+
# @param transport [Object, nil] Internal — a pre-built config transport
|
|
413
|
+
# supplied by a top-level client so the config surface shares one
|
|
414
|
+
# connection pool. Not for direct use.
|
|
415
|
+
# @param metrics [Object, nil] Internal — the parent's metrics reporter.
|
|
293
416
|
def initialize(api_key = nil, environment: nil, base_url: nil, profile: nil,
|
|
294
417
|
base_domain: nil, scheme: nil, debug: nil, extra_headers: nil,
|
|
295
418
|
parent: nil, transport: nil, metrics: nil)
|
|
@@ -338,6 +461,16 @@ module Smplkit
|
|
|
338
461
|
# +parent+ accepts either a config id (string) or an existing +Config+
|
|
339
462
|
# instance — passing the instance lets you skip naming the id explicitly
|
|
340
463
|
# when you already have the parent in scope.
|
|
464
|
+
#
|
|
465
|
+
# @param id [String] The config identifier (slug) the resource will be
|
|
466
|
+
# saved under.
|
|
467
|
+
# @param name [String, nil] Display name. Defaults to a title-cased form
|
|
468
|
+
# of +id+.
|
|
469
|
+
# @param description [String, nil] Optional human-readable description.
|
|
470
|
+
# @param parent [String, Config, nil] Optional parent config to inherit
|
|
471
|
+
# values from, as a config id or an existing +Config+ instance.
|
|
472
|
+
# @return [Config] A new, unsaved +Config+. Nothing is sent to the server
|
|
473
|
+
# until you call +Config#save+.
|
|
341
474
|
def new(id, name: nil, description: nil, parent: nil)
|
|
342
475
|
Config.new(
|
|
343
476
|
self,
|
|
@@ -350,13 +483,22 @@ module Smplkit
|
|
|
350
483
|
|
|
351
484
|
# Fetch the editable +Config+ resource by id.
|
|
352
485
|
#
|
|
353
|
-
#
|
|
486
|
+
# @param id [String] The config identifier (slug) to fetch.
|
|
487
|
+
# @return [Config] The editable +Config+ resource.
|
|
488
|
+
# @raise [Smplkit::NotFoundError] If no config with that id exists.
|
|
354
489
|
def get(id)
|
|
355
490
|
response = ApiSupport::ErrorMapping.call { @api.get_config(id) }
|
|
356
491
|
Helpers.config_from_json(self, ApiSupport::ResourceShim.from_model(response.data))
|
|
357
492
|
end
|
|
358
493
|
|
|
359
494
|
# List configs for the authenticated account.
|
|
495
|
+
#
|
|
496
|
+
# @param page_number [Integer, nil] 1-based page to fetch. When omitted,
|
|
497
|
+
# the server's default first page is returned.
|
|
498
|
+
# @param page_size [Integer, nil] Number of configs per page. When
|
|
499
|
+
# omitted, the server's default page size is used.
|
|
500
|
+
# @return [Array<Config>] The configs on the requested page, or an empty
|
|
501
|
+
# array if there are none.
|
|
360
502
|
def list(page_number: nil, page_size: nil)
|
|
361
503
|
opts = {}
|
|
362
504
|
opts[:page_number] = page_number unless page_number.nil?
|
|
@@ -366,6 +508,9 @@ module Smplkit
|
|
|
366
508
|
end
|
|
367
509
|
|
|
368
510
|
# Delete a config by id.
|
|
511
|
+
#
|
|
512
|
+
# @param id [String] The config identifier (slug) to delete.
|
|
513
|
+
# @return [void]
|
|
369
514
|
def delete(id)
|
|
370
515
|
ApiSupport::ErrorMapping.call { @api.delete_config(id) }
|
|
371
516
|
nil
|
|
@@ -386,6 +531,20 @@ module Smplkit
|
|
|
386
531
|
# ----------------------------------------------------------------
|
|
387
532
|
|
|
388
533
|
# Queue a configuration declaration for bulk-discovery upload.
|
|
534
|
+
#
|
|
535
|
+
# The declaration is buffered and sent in the background; it surfaces the
|
|
536
|
+
# config in the smplkit console even if no values are set yet.
|
|
537
|
+
#
|
|
538
|
+
# @param config_id [String] The config identifier (slug) being declared.
|
|
539
|
+
# @param service [String, nil] Name of the service declaring the config,
|
|
540
|
+
# or +nil+.
|
|
541
|
+
# @param environment [String, nil] Environment the declaration is scoped
|
|
542
|
+
# to, or +nil+.
|
|
543
|
+
# @param parent [String, nil] Optional parent config id this config
|
|
544
|
+
# inherits from.
|
|
545
|
+
# @param name [String, nil] Optional display name for the config.
|
|
546
|
+
# @param description [String, nil] Optional human-readable description.
|
|
547
|
+
# @return [void]
|
|
389
548
|
def register_config(config_id, service:, environment:, parent: nil, name: nil, description: nil)
|
|
390
549
|
@buffer.declare(config_id, service: service, environment: environment,
|
|
391
550
|
parent: parent, name: name, description: description)
|
|
@@ -393,17 +552,30 @@ module Smplkit
|
|
|
393
552
|
end
|
|
394
553
|
|
|
395
554
|
# Queue a config item declaration. +register_config+ must run first.
|
|
555
|
+
#
|
|
556
|
+
# The declaration is buffered and sent in the background, surfacing the
|
|
557
|
+
# item (with its type and default) in the smplkit console.
|
|
558
|
+
#
|
|
559
|
+
# @param config_id [String] The config identifier (slug) the item belongs
|
|
560
|
+
# to.
|
|
561
|
+
# @param item_key [String] Key of the item within the config.
|
|
562
|
+
# @param item_type [String] Item value type — one of +"STRING"+,
|
|
563
|
+
# +"NUMBER"+, +"BOOLEAN"+, or +"JSON"+.
|
|
564
|
+
# @param default [Object] The in-code default value for the item.
|
|
565
|
+
# @param description [String, nil] Optional human-readable description.
|
|
566
|
+
# @return [void]
|
|
396
567
|
def register_config_item(config_id, item_key, item_type, default, description = nil)
|
|
397
568
|
@buffer.add_item(config_id, item_key, item_type, default, description)
|
|
398
569
|
trigger_background_flush_if_needed
|
|
399
570
|
end
|
|
400
571
|
|
|
401
|
-
#
|
|
572
|
+
# Send any queued config and item declarations to the server.
|
|
573
|
+
#
|
|
574
|
+
# Discovery is best-effort — failures here never propagate to your code.
|
|
575
|
+
# Drained entries are not requeued; the SDK re-observes them on the next
|
|
576
|
+
# process start.
|
|
402
577
|
#
|
|
403
|
-
#
|
|
404
|
-
# +managed=false+ and is plan-limit-exempt — failures here never
|
|
405
|
-
# propagate to customer code. Drained entries are not requeued; the SDK
|
|
406
|
-
# will re-observe on the next process start.
|
|
578
|
+
# @return [void]
|
|
407
579
|
def flush
|
|
408
580
|
batch = @buffer.drain
|
|
409
581
|
return if batch.empty?
|
|
@@ -412,12 +584,14 @@ module Smplkit
|
|
|
412
584
|
begin
|
|
413
585
|
ApiSupport::ErrorMapping.call { @api.bulk_register_configs(body) }
|
|
414
586
|
rescue StandardError => e
|
|
415
|
-
# Fire-and-forget
|
|
587
|
+
# Fire-and-forget — discovery failures never propagate to caller code.
|
|
416
588
|
Smplkit.debug("registration", "config bulk register failed: #{e.class}: #{e.message}")
|
|
417
589
|
end
|
|
418
590
|
end
|
|
419
591
|
|
|
420
592
|
# Number of pending config declarations awaiting flush.
|
|
593
|
+
#
|
|
594
|
+
# @return [Integer] The count of buffered declarations not yet flushed.
|
|
421
595
|
def pending_count
|
|
422
596
|
@buffer.pending_count
|
|
423
597
|
end
|
|
@@ -452,6 +626,17 @@ module Smplkit
|
|
|
452
626
|
# originally-bound object; the new +config+ argument is ignored.
|
|
453
627
|
#
|
|
454
628
|
# Connects lazily on first use — no explicit install step.
|
|
629
|
+
#
|
|
630
|
+
# @param id [String] The config id to register under.
|
|
631
|
+
# @param config [Hash, Struct] A populated Hash or Struct. Both supply
|
|
632
|
+
# the schema (via the keys or Struct members) and the in-code defaults.
|
|
633
|
+
# @param parent [Hash, Struct, nil] Optional parent — any object
|
|
634
|
+
# previously returned from a +#bind+ call. Activates parent-chain
|
|
635
|
+
# inheritance for keys the caller omitted.
|
|
636
|
+
# @return [Hash, Struct] The same +config+ object, registered and live.
|
|
637
|
+
# @raise [TypeError] If +config+ is neither a Hash nor a Struct.
|
|
638
|
+
# @raise [ArgumentError] If +parent+ is provided but was not previously
|
|
639
|
+
# bound via +#bind+.
|
|
455
640
|
def bind(id, config, parent: nil)
|
|
456
641
|
ensure_connected
|
|
457
642
|
unless config.is_a?(Hash) || config.is_a?(Struct)
|
|
@@ -476,8 +661,12 @@ module Smplkit
|
|
|
476
661
|
# registers the config declaration for code-first observability so the
|
|
477
662
|
# reference appears in the smplkit console.
|
|
478
663
|
#
|
|
479
|
-
# Connects lazily on first use — no explicit install step.
|
|
480
|
-
#
|
|
664
|
+
# Connects lazily on first use — no explicit install step.
|
|
665
|
+
#
|
|
666
|
+
# @param id [String] The config identifier (slug) to subscribe to.
|
|
667
|
+
# @return [LiveConfigProxy] A live proxy whose reads always see the
|
|
668
|
+
# current resolved values.
|
|
669
|
+
# @raise [Smplkit::NotFoundError] If the config is unknown.
|
|
481
670
|
def subscribe(id)
|
|
482
671
|
ensure_connected
|
|
483
672
|
observe_config_declaration(id, parent: nil, name: nil, description: nil)
|
|
@@ -506,6 +695,19 @@ module Smplkit
|
|
|
506
695
|
# For a live dict-like view use +#subscribe+; for typed access via a
|
|
507
696
|
# Struct schema use +#bind+. Connects lazily on first use — no explicit
|
|
508
697
|
# install step.
|
|
698
|
+
#
|
|
699
|
+
# @param id [String] The config identifier (slug) to read from.
|
|
700
|
+
# @param key [String] The item key within the config.
|
|
701
|
+
# @param default [Object] Value returned when the config or key is
|
|
702
|
+
# missing. When omitted, a missing config or key raises instead of
|
|
703
|
+
# returning a fallback. Supplying a default also registers the config
|
|
704
|
+
# (if new) and the key — with its type inferred and +default+ as its
|
|
705
|
+
# value — so the reference appears in the smplkit console.
|
|
706
|
+
# @return [Object] The resolved value. When +default+ is supplied and the
|
|
707
|
+
# config or key is missing, returns +default+ instead.
|
|
708
|
+
# @raise [Smplkit::NotFoundError] If the config is unknown and no
|
|
709
|
+
# +default+ was supplied.
|
|
710
|
+
# @raise [KeyError] If the key is absent and no +default+ was supplied.
|
|
509
711
|
def get_value(id, key, default = MISSING)
|
|
510
712
|
ensure_connected
|
|
511
713
|
key = key.to_s
|
|
@@ -541,6 +743,14 @@ module Smplkit
|
|
|
541
743
|
# client.config.on_change("id", item_key: "key") { |event| ... } # item-scoped
|
|
542
744
|
#
|
|
543
745
|
# Connects lazily on first use — no explicit install step.
|
|
746
|
+
#
|
|
747
|
+
# @param config_id [String, nil] When given, restrict the listener to
|
|
748
|
+
# changes of this config. Omit for a global listener.
|
|
749
|
+
# @param item_key [String, nil] When +config_id+ is given, restrict the
|
|
750
|
+
# listener to changes of this single item key.
|
|
751
|
+
# @yieldparam event [ConfigChangeEvent] The change that fired the
|
|
752
|
+
# listener.
|
|
753
|
+
# @return [Proc] The registered block, unchanged.
|
|
544
754
|
def on_change(config_id = nil, item_key: nil, &block)
|
|
545
755
|
ensure_connected
|
|
546
756
|
raise ArgumentError, "on_change requires a block" unless block
|
|
@@ -553,6 +763,9 @@ module Smplkit
|
|
|
553
763
|
# listeners for anything that differs from the previous state.
|
|
554
764
|
#
|
|
555
765
|
# Connects lazily on first use — no explicit install step.
|
|
766
|
+
#
|
|
767
|
+
# @return [void]
|
|
768
|
+
# @raise [Smplkit::ConnectionError] If the fetch fails.
|
|
556
769
|
def refresh
|
|
557
770
|
ensure_connected
|
|
558
771
|
do_refresh("manual")
|
|
@@ -564,6 +777,8 @@ module Smplkit
|
|
|
564
777
|
# live use) and the owned HTTP transport (standalone construction). A
|
|
565
778
|
# wired client borrows the parent's transport and WebSocket and closes
|
|
566
779
|
# neither.
|
|
780
|
+
#
|
|
781
|
+
# @return [void]
|
|
567
782
|
def close
|
|
568
783
|
if @owns_ws && @ws_manager
|
|
569
784
|
@ws_manager.stop
|
|
@@ -575,6 +790,11 @@ module Smplkit
|
|
|
575
790
|
alias _close close
|
|
576
791
|
|
|
577
792
|
# Construct, yield to the block, and close on exit.
|
|
793
|
+
#
|
|
794
|
+
# @param kwargs [Hash] Keyword arguments forwarded to +#initialize+.
|
|
795
|
+
# @yieldparam client [ConfigClient] The constructed client, closed when
|
|
796
|
+
# the block returns.
|
|
797
|
+
# @return [Object] The value returned by the block.
|
|
578
798
|
def self.open(**kwargs)
|
|
579
799
|
client = new(**kwargs)
|
|
580
800
|
begin
|
|
@@ -888,21 +1108,53 @@ module Smplkit
|
|
|
888
1108
|
fire_change_listeners(old_cache, new_cache, source: source)
|
|
889
1109
|
end
|
|
890
1110
|
|
|
1111
|
+
# Pull any referenced-but-uncached parent (and ancestors) into +store+.
|
|
1112
|
+
#
|
|
1113
|
+
# A config_changed event fetches only the changed config. If that config
|
|
1114
|
+
# inherits from a parent that isn't already in the store — e.g. a parent
|
|
1115
|
+
# created via discovery after the initial connect that never broadcast
|
|
1116
|
+
# its own event — the chain walk in +rebuild_from_store+ would stop at
|
|
1117
|
+
# the gap and the child would re-resolve missing its inherited values.
|
|
1118
|
+
# Walk every config's parent pointers and fetch each absent ancestor so
|
|
1119
|
+
# the inheritance chain resolves fully.
|
|
1120
|
+
def ensure_ancestors_cached(store)
|
|
1121
|
+
pending = store.values.filter_map { |cfg| parent_pointer(cfg) }
|
|
1122
|
+
until pending.empty?
|
|
1123
|
+
parent_id = pending.pop
|
|
1124
|
+
next if store.key?(parent_id)
|
|
1125
|
+
|
|
1126
|
+
parent = fetch_config(parent_id)
|
|
1127
|
+
next if parent.nil?
|
|
1128
|
+
|
|
1129
|
+
store[parent_id] = parent
|
|
1130
|
+
grandparent = parent_pointer(parent)
|
|
1131
|
+
pending << grandparent unless grandparent.nil?
|
|
1132
|
+
end
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
# The parent config id a +Config+ points at, or +nil+ for a root —
|
|
1136
|
+
# treating an empty-string parent the same as none, matching the
|
|
1137
|
+
# break condition in +Helpers.build_chain+.
|
|
1138
|
+
def parent_pointer(config)
|
|
1139
|
+
pid = config.parent_id
|
|
1140
|
+
pid unless pid.nil? || pid == ""
|
|
1141
|
+
end
|
|
1142
|
+
|
|
891
1143
|
def handle_config_changed(data)
|
|
892
1144
|
key = data["id"] || data["key"]
|
|
893
1145
|
return handle_configs_changed(data) unless key
|
|
894
1146
|
|
|
895
|
-
new_store = @lock.synchronize { @raw_config_store.dup }
|
|
896
1147
|
begin
|
|
1148
|
+
new_store = @lock.synchronize { @raw_config_store.dup }
|
|
897
1149
|
cfg = fetch_config(key)
|
|
1150
|
+
return if cfg.nil?
|
|
1151
|
+
|
|
1152
|
+
new_store[key] = cfg
|
|
1153
|
+
ensure_ancestors_cached(new_store)
|
|
1154
|
+
rebuild_from_store(new_store, source: "websocket")
|
|
898
1155
|
rescue StandardError => e
|
|
899
|
-
Smplkit.debug("config", "
|
|
900
|
-
return
|
|
1156
|
+
Smplkit.debug("config", "config_changed handler failed for #{key.inspect}: #{e.class}: #{e.message}")
|
|
901
1157
|
end
|
|
902
|
-
return if cfg.nil?
|
|
903
|
-
|
|
904
|
-
new_store[key] = cfg
|
|
905
|
-
rebuild_from_store(new_store, source: "websocket")
|
|
906
1158
|
end
|
|
907
1159
|
|
|
908
1160
|
def handle_config_deleted(data)
|
|
@@ -954,8 +1206,8 @@ module Smplkit
|
|
|
954
1206
|
def config_envs_to_wire(environments)
|
|
955
1207
|
return nil if environments.empty?
|
|
956
1208
|
|
|
957
|
-
#
|
|
958
|
-
#
|
|
1209
|
+
# The wire shape for env overrides is a flat +{env: {key: rawValue}}+
|
|
1210
|
+
# map — no envelope, no per-key type wrapper.
|
|
959
1211
|
environments.each_with_object({}) do |(env_key, env_obj), out|
|
|
960
1212
|
out[env_key] = env_obj.values
|
|
961
1213
|
end
|
|
@@ -991,4 +1243,5 @@ module Smplkit
|
|
|
991
1243
|
end
|
|
992
1244
|
|
|
993
1245
|
ConfigClient = Config::ConfigClient
|
|
1246
|
+
ConfigChangeEvent = Config::ConfigChangeEvent
|
|
994
1247
|
end
|
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Smplkit
|
|
4
4
|
module Config
|
|
5
|
+
# Internal conversion and resolution helpers shared by the config client.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
5
8
|
module Helpers
|
|
6
9
|
module_function
|
|
7
10
|
|
|
8
11
|
# Translate a JSON:API resource Hash into a Config domain model.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
# @param client [ConfigClient, nil] The owning client, or +nil+ for a
|
|
15
|
+
# detached model.
|
|
16
|
+
# @param resource [Hash{String => Object}] The JSON:API resource Hash.
|
|
17
|
+
# @return [Config] The constructed config domain model.
|
|
9
18
|
def config_from_json(client, resource)
|
|
10
19
|
attrs = resource["attributes"] || {}
|
|
11
20
|
items = (attrs["items"] || {}).map do |name, item|
|
|
@@ -22,8 +31,8 @@ module Smplkit
|
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
environments = (attrs["environments"] || {}).each_with_object({}) do |(env, env_data), out|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
34
|
+
# env_data is already the flat override map +{key: rawValue}+ — the
|
|
35
|
+
# old +{values: {...}}+ envelope is gone.
|
|
27
36
|
env_values = env_data.is_a?(Hash) ? env_data : {}
|
|
28
37
|
out[env] = ConfigEnvironment.new(values: env_values)
|
|
29
38
|
end
|
|
@@ -44,6 +53,12 @@ module Smplkit
|
|
|
44
53
|
|
|
45
54
|
# Deep-merge two Hashes, with +override+ winning. Mirrors the Python
|
|
46
55
|
# +deep_merge+ helper used by the resolver.
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
# @param base [Hash{String => Object}] The base Hash.
|
|
59
|
+
# @param override [Hash{String => Object}] The Hash whose values win on
|
|
60
|
+
# conflict.
|
|
61
|
+
# @return [Hash{String => Object}] A new merged Hash.
|
|
47
62
|
def deep_merge(base, override)
|
|
48
63
|
result = base.dup
|
|
49
64
|
override.each do |key, value|
|
|
@@ -57,6 +72,10 @@ module Smplkit
|
|
|
57
72
|
end
|
|
58
73
|
|
|
59
74
|
# Unwrap typed items +{ key => { value, type, desc } }+ to +{ key => raw }+.
|
|
75
|
+
#
|
|
76
|
+
# @api private
|
|
77
|
+
# @param items [Hash{String => Object}] Typed items keyed by item key.
|
|
78
|
+
# @return [Hash{String => Object}] The items as +{ key => raw_value }+.
|
|
60
79
|
def unwrap_items(items)
|
|
61
80
|
items.each_with_object({}) do |(k, v), out|
|
|
62
81
|
out[k] = v.is_a?(Hash) && v.key?("value") ? v["value"] : v
|
|
@@ -66,6 +85,13 @@ module Smplkit
|
|
|
66
85
|
# Build the parent chain (child-first, root-last) for a +Config+,
|
|
67
86
|
# walking +parent_id+ pointers across the +by_id+ map. Mirrors the
|
|
68
87
|
# Python SDK's client-side chain construction.
|
|
88
|
+
#
|
|
89
|
+
# @api private
|
|
90
|
+
# @param target [Config] The config to build the chain for.
|
|
91
|
+
# @param by_id [Hash{String => Config}] Pre-fetched configs keyed by id,
|
|
92
|
+
# used to look up parents without extra network calls.
|
|
93
|
+
# @return [Array<Hash{String => Object}>] Chain entries from child to
|
|
94
|
+
# root.
|
|
69
95
|
def build_chain(target, by_id)
|
|
70
96
|
chain = []
|
|
71
97
|
current = target
|
|
@@ -84,14 +110,19 @@ module Smplkit
|
|
|
84
110
|
|
|
85
111
|
# Build a single chain entry (the +id+/+items+/+environments+ Hash
|
|
86
112
|
# shape used by +resolve_chain+) from a +Config+ domain model.
|
|
113
|
+
#
|
|
114
|
+
# @api private
|
|
115
|
+
# @param config [Config] The config to convert.
|
|
116
|
+
# @return [Hash{String => Object}] An +id+/+items+/+environments+ chain
|
|
117
|
+
# entry.
|
|
87
118
|
def config_to_chain_entry(config)
|
|
88
119
|
items_hash = config.items.to_h do |item|
|
|
89
120
|
[item.name,
|
|
90
121
|
{ "value" => item.value, "type" => item.type, "description" => item.description }.compact]
|
|
91
122
|
end
|
|
92
123
|
environments = config.environments.each_with_object({}) do |(env_key, env_obj), out|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
124
|
+
# Env entries are flat +{key: rawValue}+ maps — no +values+
|
|
125
|
+
# envelope, no per-key type wrapper.
|
|
95
126
|
out[env_key] = env_obj.values
|
|
96
127
|
end
|
|
97
128
|
{ "id" => config.id, "items" => items_hash, "environments" => environments }
|
|
@@ -101,13 +132,20 @@ module Smplkit
|
|
|
101
132
|
#
|
|
102
133
|
# Walks from root (last element) to child (first element), accumulating
|
|
103
134
|
# values via deep merge so child configs override parent configs.
|
|
135
|
+
#
|
|
136
|
+
# @api private
|
|
137
|
+
# @param chain [Array<Hash{String => Object}>] Chain entries from child
|
|
138
|
+
# to root.
|
|
139
|
+
# @param environment [String, nil] The environment whose overrides to
|
|
140
|
+
# apply, or +nil+ for base values only.
|
|
141
|
+
# @return [Hash{String => Object}] The resolved +{key => value}+ map.
|
|
104
142
|
def resolve_chain(chain, environment)
|
|
105
143
|
accumulated = {}
|
|
106
144
|
chain.reverse_each do |config_data|
|
|
107
145
|
raw_items = config_data["items"] || config_data["values"] || {}
|
|
108
146
|
base_values = unwrap_items(raw_items)
|
|
109
|
-
#
|
|
110
|
-
# the
|
|
147
|
+
# Env entries are flat +{key: rawValue}+ maps — the resolver reads
|
|
148
|
+
# the env entry directly as the override map.
|
|
111
149
|
env_data = (config_data["environments"] || {})[environment] || {}
|
|
112
150
|
env_values = env_data.is_a?(Hash) ? env_data : {}
|
|
113
151
|
config_resolved = deep_merge(base_values, env_values)
|