smplkit 3.0.95 → 3.0.96

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.
@@ -1,1074 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Smplkit
6
- # Top-level management client. Owns the HTTP transports + CRUD APIs for
7
- # every resource on the smplkit platform.
8
- #
9
- # Sub-namespaces (mirroring the Python SDK):
10
- #
11
- # - +mgmt.contexts.*+
12
- # - +mgmt.context_types.*+
13
- # - +mgmt.environments.*+
14
- # - +mgmt.services.*+
15
- # - +mgmt.account_settings.*+
16
- # - +mgmt.config.*+
17
- # - +mgmt.flags.*+
18
- # - +mgmt.loggers.*+
19
- # - +mgmt.log_groups.*+
20
- #
21
- # Constructable both as +Smplkit::ManagementClient.new+ (standalone) and as
22
- # +Smplkit::Client#manage+ (shared transports).
23
- #
24
- # Each namespace is wired to a generated +SmplkitGeneratedClient+ +ApiClient+
25
- # under the hood — auth, request encoding, and response parsing flow through
26
- # the openapi-generator-produced layer in +lib/smplkit/_generated+. The
27
- # wrapper layer keeps the customer-facing domain models (+Flag+, +Config+,
28
- # etc.) and converts at the boundary via the existing
29
- # +<resource>_from_resource+ helpers.
30
- class ManagementClient
31
- # Default page[size] the runtime asks for when walking a list
32
- # endpoint to completion. The platform caps page[size] at 1000;
33
- # using the same value here makes the minimum number of round-trips
34
- # per exhaustive fetch.
35
- RUNTIME_PAGE_SIZE = 1000
36
-
37
- attr_reader :contexts, :context_types, :environments, :services, :account_settings,
38
- :config, :flags, :loggers, :log_groups, :audit, :jobs
39
-
40
- def self.from_resolved(resolved, extra_headers: nil)
41
- new(_resolved: resolved, extra_headers: extra_headers)
42
- end
43
-
44
- def initialize(api_key: nil, base_domain: nil, scheme: nil, profile: nil,
45
- debug: nil, _resolved: nil, extra_headers: nil)
46
- cfg = _resolved ||
47
- ConfigResolution.resolve_management_config(
48
- api_key: api_key, base_domain: base_domain, scheme: scheme,
49
- profile: profile, debug: debug
50
- )
51
- Smplkit.enable_debug if cfg.debug
52
-
53
- @resolved = cfg
54
-
55
- @extra_headers = extra_headers
56
- @app_api_client = build_api_client(SmplkitGeneratedClient::App, "app", cfg)
57
- @config_api_client = build_api_client(SmplkitGeneratedClient::Config, "config", cfg)
58
- @flags_api_client = build_api_client(SmplkitGeneratedClient::Flags, "flags", cfg)
59
- @logging_api_client = build_api_client(SmplkitGeneratedClient::Logging, "logging", cfg)
60
- @audit_api_client = build_api_client(SmplkitGeneratedClient::Audit, "audit", cfg)
61
- @jobs_api_client = build_api_client(SmplkitGeneratedClient::Jobs, "jobs", cfg)
62
-
63
- @contexts = ContextsNamespace.new(@app_api_client)
64
- @context_types = ContextTypesNamespace.new(@app_api_client)
65
- @environments = EnvironmentsNamespace.new(@app_api_client)
66
- @services = ServicesNamespace.new(@app_api_client)
67
- @account_settings = AccountSettingsNamespace.new(@app_api_client)
68
- @config = ConfigNamespace.new(@config_api_client)
69
- @flags = FlagsNamespace.new(@flags_api_client)
70
- @loggers = LoggersNamespace.new(@logging_api_client)
71
- @log_groups = LogGroupsNamespace.new(@logging_api_client)
72
- @audit = Management::AuditNamespace.new(@audit_api_client)
73
- @jobs = Management::JobsNamespace.new(@jobs_api_client)
74
- end
75
-
76
- def close
77
- # The generated ApiClient owns Faraday connections that release on GC.
78
- # No explicit shutdown is exposed; this stub keeps the API stable.
79
- end
80
-
81
- def _resolved = @resolved
82
- def _app_http = @app_api_client
83
- def _config_http = @config_api_client
84
- def _flags_http = @flags_api_client
85
- def _logging_http = @logging_api_client
86
- def _audit_http = @audit_api_client
87
- def _jobs_http = @jobs_api_client
88
-
89
- SDK_OWNED_HEADERS = %w[authorization content-type user-agent].freeze
90
-
91
- private
92
-
93
- def build_api_client(generated_module, subdomain, cfg)
94
- configuration = generated_module::Configuration.new
95
- configuration.scheme = cfg.scheme
96
- configuration.host = "#{subdomain}.#{cfg.base_domain}"
97
- configuration.base_path = ""
98
- configuration.access_token = cfg.api_key
99
- configuration.debugging = cfg.debug
100
- HttpPool.configure(configuration)
101
- generated_module::ApiClient.new(configuration).tap do |client|
102
- client.default_headers["User-Agent"] = "smplkit-ruby-sdk/#{Smplkit::VERSION}"
103
- @extra_headers&.each do |k, v|
104
- client.default_headers[k] = v unless SDK_OWNED_HEADERS.include?(k.downcase)
105
- end
106
- end
107
- end
108
-
109
- # ------------------------------------------------------------------
110
- # Shared error-mapping wrapper
111
- # ------------------------------------------------------------------
112
-
113
- # Wraps a generated-API call and converts any +ApiError+ raised by the
114
- # generated layer into the +Smplkit::Error+ hierarchy. Connection-level
115
- # failures (no response from the server) become +Smplkit::ConnectionError+;
116
- # status-coded failures route through +Errors.raise_for_status+ which
117
- # emits +NotFoundError+ / +ConflictError+ / +ValidationError+ / +Error+
118
- # depending on the JSON:API body.
119
- module ErrorMapping
120
- module_function
121
-
122
- def call
123
- yield
124
- rescue StandardError => e
125
- raise unless generated_api_error?(e)
126
-
127
- raise Smplkit::ConnectionError, e.message.to_s if e.code.nil? || e.code.zero?
128
-
129
- Smplkit::Errors.raise_for_status(e.code, e.response_body.to_s)
130
- # raise_for_status only returns on 2xx; if we get here the generated
131
- # layer raised on a 2xx (shouldn't happen) so re-raise the original.
132
- raise
133
- end
134
-
135
- def generated_api_error?(err)
136
- klass_name = err.class.name.to_s
137
- klass_name.start_with?("SmplkitGeneratedClient::") && klass_name.end_with?("::ApiError")
138
- end
139
- end
140
-
141
- # Walk a generated paginated list endpoint to completion.
142
- #
143
- # The block receives a per-page +opts+ hash with +page_number+ and
144
- # +page_size+ filled in, calls the generated list method through
145
- # {ErrorMapping.call}, and returns the response object. Pages stop
146
- # when the server returns fewer rows than requested — the platform's
147
- # standard last-page signal across every offset-paginated list
148
- # endpoint. Returns the concatenated +response.data+ rows.
149
- module PaginatedFetch
150
- module_function
151
-
152
- def collect(page_size: RUNTIME_PAGE_SIZE)
153
- rows = []
154
- page_number = 1
155
- loop do
156
- opts = { page_number: page_number, page_size: page_size }
157
- response = ErrorMapping.call { yield(opts) }
158
- page = response.data || []
159
- rows.concat(page)
160
- break if page.length < page_size
161
-
162
- page_number += 1
163
- end
164
- rows
165
- end
166
- end
167
-
168
- # Deep-stringify Hash keys so resources returned by generated +to_hash+
169
- # (symbol-keyed) match what the wrapper helpers expect (string-keyed).
170
- module ResourceShim
171
- module_function
172
-
173
- def stringify(value)
174
- Smplkit::Helpers.deep_stringify_keys(value)
175
- end
176
-
177
- # Convenience: produce a string-keyed Hash from a generated model.
178
- def from_model(model)
179
- return {} if model.nil?
180
-
181
- stringify(model.to_hash)
182
- end
183
- end
184
-
185
- # ------------------------------------------------------------------
186
- # Sub-namespaces
187
- # ------------------------------------------------------------------
188
-
189
- class ContextsNamespace
190
- def initialize(api_client)
191
- @api = SmplkitGeneratedClient::App::ContextsApi.new(api_client)
192
- @buffer = Management::ContextRegistrationBuffer.new
193
- end
194
-
195
- def register(contexts)
196
- return if contexts.nil? || contexts.empty?
197
-
198
- @buffer.observe(contexts)
199
- flush if @buffer.pending_count >= Management::CONTEXT_BATCH_FLUSH_SIZE
200
- end
201
-
202
- def flush
203
- batch = @buffer.drain
204
- return if batch.empty?
205
-
206
- items = batch.map do |entry|
207
- SmplkitGeneratedClient::App::ContextBulkItem.new(
208
- type: entry["type"], key: entry["key"], attributes: entry["attributes"] || {}
209
- )
210
- end
211
- body = SmplkitGeneratedClient::App::ContextBulkRegister.new(contexts: items)
212
- ErrorMapping.call { @api.bulk_register_contexts(body) }
213
- rescue StandardError => e
214
- Smplkit.debug("registration", "context flush failed: #{e.class}: #{e.message}")
215
- end
216
-
217
- def list(page_number: nil, page_size: nil)
218
- opts = {}
219
- opts[:page_number] = page_number unless page_number.nil?
220
- opts[:page_size] = page_size unless page_size.nil?
221
- response = ErrorMapping.call { @api.list_contexts(opts) }
222
- (response.data || []).map { |r| context_from_resource(ResourceShim.from_model(r)) }
223
- end
224
-
225
- def get(id_or_type, key = nil)
226
- type, ckey = split_id(id_or_type, key)
227
- response = ErrorMapping.call { @api.get_context("#{type}:#{ckey}") }
228
- context_from_resource(ResourceShim.from_model(response.data))
229
- end
230
-
231
- def delete(id_or_type, key = nil)
232
- type, ckey = split_id(id_or_type, key)
233
- ErrorMapping.call { @api.delete_context("#{type}:#{ckey}") }
234
- true
235
- end
236
-
237
- def _save_context(ctx)
238
- body = SmplkitGeneratedClient::App::ContextResponse.new(
239
- data: SmplkitGeneratedClient::App::ContextResource.new(
240
- type: "context",
241
- id: ctx.id,
242
- attributes: SmplkitGeneratedClient::App::Context.new(
243
- name: ctx.name, context_type: ctx.type, attributes: ctx.attributes
244
- )
245
- )
246
- )
247
- response = ErrorMapping.call { @api.update_context(ctx.id, body) }
248
- context_from_resource(ResourceShim.from_model(response.data)).tap { |c| c._bind_client(self) }
249
- end
250
-
251
- private
252
-
253
- def split_id(id_or_type, key)
254
- return [id_or_type, key] if key
255
-
256
- unless id_or_type.include?(":")
257
- raise ArgumentError, "context id must be 'type:key' (got #{id_or_type.inspect}); " \
258
- "alternatively pass type and key as separate args"
259
- end
260
-
261
- id_or_type.split(":", 2)
262
- end
263
-
264
- def context_from_resource(resource)
265
- attrs = resource["attributes"] || {}
266
- Smplkit::Context.new(
267
- attrs["context_type"] || attrs["type"] || resource["id"].to_s.split(":").first,
268
- attrs["key"] || resource["id"].to_s.split(":", 2).last,
269
- attrs["attributes"] || {},
270
- name: attrs["name"],
271
- created_at: attrs["created_at"],
272
- updated_at: attrs["updated_at"]
273
- )._bind_client(self)
274
- end
275
- end
276
-
277
- class ContextTypesNamespace
278
- def initialize(api_client)
279
- @api = SmplkitGeneratedClient::App::ContextTypesApi.new(api_client)
280
- end
281
-
282
- def list(page_number: nil, page_size: nil)
283
- opts = {}
284
- opts[:page_number] = page_number unless page_number.nil?
285
- opts[:page_size] = page_size unless page_size.nil?
286
- response = ErrorMapping.call { @api.list_context_types(opts) }
287
- (response.data || []).map { |r| from_resource(ResourceShim.from_model(r)) }
288
- end
289
-
290
- def get(key)
291
- response = ErrorMapping.call { @api.get_context_type(key) }
292
- from_resource(ResourceShim.from_model(response.data))
293
- end
294
-
295
- def delete(key)
296
- ErrorMapping.call { @api.delete_context_type(key) }
297
- true
298
- end
299
-
300
- def new_context_type(key, name: nil, description: nil)
301
- Management::ContextType.new(self, key: key, name: name, description: description)
302
- end
303
-
304
- def _create_context_type(ct)
305
- response = ErrorMapping.call { @api.create_context_type(body_for(ct)) }
306
- from_resource(ResourceShim.from_model(response.data))
307
- end
308
-
309
- def _update_context_type(ct)
310
- response = ErrorMapping.call { @api.update_context_type(ct.key, body_for(ct)) }
311
- from_resource(ResourceShim.from_model(response.data))
312
- end
313
-
314
- private
315
-
316
- def body_for(ct)
317
- # ContextType server schema: name, attributes, created_at, updated_at.
318
- # Customer-side +description+ is wrapper-only; not sent on the wire.
319
- SmplkitGeneratedClient::App::ContextTypeResponse.new(
320
- data: SmplkitGeneratedClient::App::ContextTypeResource.new(
321
- type: "context_type",
322
- id: ct.key,
323
- attributes: SmplkitGeneratedClient::App::ContextType.new(name: ct.name)
324
- )
325
- )
326
- end
327
-
328
- def from_resource(resource)
329
- attrs = resource["attributes"] || {}
330
- Management::ContextType.new(
331
- self,
332
- id: resource["id"], key: attrs["key"] || resource["id"],
333
- name: attrs["name"], description: attrs["description"],
334
- created_at: attrs["created_at"], updated_at: attrs["updated_at"]
335
- )
336
- end
337
- end
338
-
339
- class EnvironmentsNamespace
340
- def initialize(api_client)
341
- @api = SmplkitGeneratedClient::App::EnvironmentsApi.new(api_client)
342
- end
343
-
344
- def list(page_number: nil, page_size: nil)
345
- opts = {}
346
- opts[:page_number] = page_number unless page_number.nil?
347
- opts[:page_size] = page_size unless page_size.nil?
348
- response = ErrorMapping.call { @api.list_environments(opts) }
349
- (response.data || []).map { |r| from_resource(ResourceShim.from_model(r)) }
350
- end
351
-
352
- def get(key)
353
- response = ErrorMapping.call { @api.get_environment(key) }
354
- from_resource(ResourceShim.from_model(response.data))
355
- end
356
-
357
- def delete(key)
358
- ErrorMapping.call { @api.delete_environment(key) }
359
- true
360
- end
361
-
362
- def new(key, name: nil, color: nil,
363
- classification: Management::EnvironmentClassification::STANDARD,
364
- description: nil)
365
- color = Management::Color.new(color) if color.is_a?(String)
366
- Management::Environment.new(
367
- self,
368
- key: key, name: name || Smplkit::Helpers.key_to_display_name(key),
369
- color: color, classification: classification, description: description
370
- )
371
- end
372
-
373
- def _create_environment(env)
374
- response = ErrorMapping.call { @api.create_environment(body_for(env)) }
375
- from_resource(ResourceShim.from_model(response.data))
376
- end
377
-
378
- def _update_environment(env)
379
- response = ErrorMapping.call { @api.update_environment(env.key, body_for(env)) }
380
- from_resource(ResourceShim.from_model(response.data))
381
- end
382
-
383
- private
384
-
385
- def body_for(env)
386
- # Environment server schema: name, color, classification.
387
- # Customer-side +description+ stays wrapper-only.
388
- SmplkitGeneratedClient::App::EnvironmentResponse.new(
389
- data: SmplkitGeneratedClient::App::EnvironmentResource.new(
390
- type: "environment",
391
- id: env.key,
392
- attributes: SmplkitGeneratedClient::App::Environment.new(
393
- name: env.name,
394
- color: env.color&.hex,
395
- classification: env.classification
396
- )
397
- )
398
- )
399
- end
400
-
401
- def from_resource(resource)
402
- attrs = resource["attributes"] || {}
403
- color = attrs["color"] && Management::Color.new(attrs["color"])
404
- Management::Environment.new(
405
- self,
406
- id: resource["id"], key: attrs["key"] || resource["id"],
407
- name: attrs["name"], color: color,
408
- classification: attrs["classification"] || Management::EnvironmentClassification::STANDARD,
409
- description: attrs["description"],
410
- created_at: attrs["created_at"], updated_at: attrs["updated_at"]
411
- )
412
- end
413
- end
414
-
415
- class ServicesNamespace
416
- def initialize(api_client)
417
- @api = SmplkitGeneratedClient::App::ServicesApi.new(api_client)
418
- end
419
-
420
- def list(page_number: nil, page_size: nil)
421
- opts = {}
422
- opts[:page_number] = page_number unless page_number.nil?
423
- opts[:page_size] = page_size unless page_size.nil?
424
- response = ErrorMapping.call { @api.list_services(opts) }
425
- (response.data || []).map { |r| from_resource(ResourceShim.from_model(r)) }
426
- end
427
-
428
- def get(key)
429
- response = ErrorMapping.call { @api.get_service(key) }
430
- from_resource(ResourceShim.from_model(response.data))
431
- end
432
-
433
- def delete(key)
434
- ErrorMapping.call { @api.delete_service(key) }
435
- true
436
- end
437
-
438
- def new(key, name: nil)
439
- Management::Service.new(
440
- self,
441
- key: key,
442
- name: name || Smplkit::Helpers.key_to_display_name(key)
443
- )
444
- end
445
-
446
- def _create_service(svc)
447
- response = ErrorMapping.call { @api.create_service(create_body_for(svc)) }
448
- from_resource(ResourceShim.from_model(response.data))
449
- end
450
-
451
- def _update_service(svc)
452
- response = ErrorMapping.call { @api.update_service(svc.key, body_for(svc)) }
453
- from_resource(ResourceShim.from_model(response.data))
454
- end
455
-
456
- private
457
-
458
- def body_for(svc)
459
- SmplkitGeneratedClient::App::ServiceRequest.new(
460
- data: SmplkitGeneratedClient::App::ServiceResource.new(
461
- type: "service",
462
- id: svc.key,
463
- attributes: SmplkitGeneratedClient::App::Service.new(
464
- name: svc.name
465
- )
466
- )
467
- )
468
- end
469
-
470
- def create_body_for(svc)
471
- SmplkitGeneratedClient::App::ServiceCreateRequest.new(
472
- data: SmplkitGeneratedClient::App::ServiceCreateResource.new(
473
- type: "service",
474
- id: svc.key,
475
- attributes: SmplkitGeneratedClient::App::Service.new(
476
- name: svc.name
477
- )
478
- )
479
- )
480
- end
481
-
482
- def from_resource(resource)
483
- attrs = resource["attributes"] || {}
484
- Management::Service.new(
485
- self,
486
- id: resource["id"], key: attrs["key"] || resource["id"],
487
- name: attrs["name"],
488
- created_at: attrs["created_at"], updated_at: attrs["updated_at"]
489
- )
490
- end
491
- end
492
-
493
- class AccountSettingsNamespace
494
- def initialize(api_client)
495
- @api = SmplkitGeneratedClient::App::AccountApi.new(api_client)
496
- end
497
-
498
- def get
499
- raw = ErrorMapping.call { @api.get_account_settings }
500
- from_raw(raw)
501
- end
502
-
503
- def _update_account_settings(settings)
504
- # The generator pulled this op without wiring a body parameter
505
- # (the server accepts a free-form JSON object). The +debug_body+
506
- # opt is the documented escape hatch.
507
- raw = ErrorMapping.call do
508
- @api.put_account_settings(debug_body: settings_body(settings))
509
- end
510
- from_raw(raw)
511
- end
512
-
513
- private
514
-
515
- def settings_body(settings)
516
- {
517
- "environment_order" => settings.environment_order,
518
- "default_environment" => settings.default_environment
519
- }.compact
520
- end
521
-
522
- def from_raw(raw)
523
- attrs = raw.respond_to?(:to_hash) ? ResourceShim.stringify(raw.to_hash) : (raw || {})
524
- if attrs.is_a?(Hash) && attrs["data"].is_a?(Hash) && attrs["data"]["attributes"]
525
- attrs = attrs["data"]["attributes"]
526
- end
527
- Management::AccountSettings.new(
528
- self,
529
- id: attrs["id"],
530
- environment_order: attrs["environment_order"] || [],
531
- default_environment: attrs["default_environment"],
532
- updated_at: attrs["updated_at"]
533
- )
534
- end
535
- end
536
-
537
- class ConfigNamespace
538
- def initialize(api_client)
539
- @api = SmplkitGeneratedClient::Config::ConfigsApi.new(api_client)
540
- @buffer = Management::ConfigRegistrationBuffer.new
541
- end
542
-
543
- # ---------------------------------------------------------------
544
- # Discovery API (ADR-037 §2.13/§2.14)
545
- # ---------------------------------------------------------------
546
-
547
- # Queue a configuration declaration for bulk-discovery upload.
548
- # Called from +ConfigClient#bind+ and +ConfigClient#get(id, key,
549
- # default)+. Threshold-flushes on a background thread once the
550
- # pending buffer reaches the flush size.
551
- def register_config(config_id, service:, environment:, parent: nil,
552
- name: nil, description: nil)
553
- @buffer.declare(config_id, service: service, environment: environment,
554
- parent: parent, name: name, description: description)
555
- trigger_background_flush_if_needed
556
- end
557
-
558
- # Queue a config item declaration. +register_config+ must have run
559
- # first; items added without a prior declaration are dropped.
560
- def register_config_item(config_id, item_key, item_type, default, description = nil)
561
- @buffer.add_item(config_id, item_key, item_type, default, description)
562
- trigger_background_flush_if_needed
563
- end
564
-
565
- def pending_count
566
- @buffer.pending_count
567
- end
568
-
569
- # Send any pending config declarations to
570
- # +POST /api/v1/configs/bulk+. Per ADR-024 §2.9 the bulk endpoint is
571
- # plan-limit-exempt; failures here never propagate to customer code.
572
- def flush
573
- batch = @buffer.drain
574
- return if batch.empty?
575
-
576
- items = batch.map do |entry|
577
- SmplkitGeneratedClient::Config::ConfigBulkItem.new(
578
- id: entry["id"],
579
- service: entry["service"],
580
- environment: entry["environment"],
581
- parent: entry["parent"],
582
- name: entry["name"],
583
- description: entry["description"],
584
- items: bulk_items_to_wire(entry["items"])
585
- )
586
- end
587
- body = SmplkitGeneratedClient::Config::ConfigBulkRequest.new(configs: items)
588
- begin
589
- ErrorMapping.call { @api.bulk_register_configs(body) }
590
- rescue StandardError => e
591
- # Fire-and-forget per ADR-024 §2.9.
592
- Smplkit.debug("registration", "config bulk register failed: #{e.class}: #{e.message}")
593
- end
594
- end
595
-
596
- def list(page_number: nil, page_size: nil)
597
- opts = {}
598
- opts[:page_number] = page_number unless page_number.nil?
599
- opts[:page_size] = page_size unless page_size.nil?
600
- response = ErrorMapping.call { @api.list_configs(opts) }
601
- (response.data || []).map { |r| Smplkit::Config::Helpers.config_from_json(self, ResourceShim.from_model(r)) }
602
- end
603
-
604
- def get(key)
605
- response = ErrorMapping.call { @api.get_config(key) }
606
- Smplkit::Config::Helpers.config_from_json(self, ResourceShim.from_model(response.data))
607
- end
608
-
609
- def delete(key)
610
- ErrorMapping.call { @api.delete_config(key) }
611
- true
612
- end
613
-
614
- def new_config(key, name: nil, description: nil, parent: nil)
615
- Smplkit::Config::Config.new(
616
- self,
617
- key: key,
618
- name: name || Smplkit::Helpers.key_to_display_name(key),
619
- description: description,
620
- parent_id: parent.is_a?(Smplkit::Config::Config) ? parent.key : parent
621
- )
622
- end
623
-
624
- def _create_config(config)
625
- response = ErrorMapping.call { @api.create_config(config_body(config)) }
626
- Smplkit::Config::Helpers.config_from_json(self, ResourceShim.from_model(response.data))
627
- end
628
-
629
- def _update_config(config)
630
- response = ErrorMapping.call { @api.update_config(config.key, config_body(config)) }
631
- Smplkit::Config::Helpers.config_from_json(self, ResourceShim.from_model(response.data))
632
- end
633
-
634
- # Walk every page of +list_configs+ so an account with more than
635
- # +RUNTIME_PAGE_SIZE+ configs still resolves to the complete set. Used
636
- # by the runtime client to refresh the resolved cache.
637
- def list_all
638
- rows = PaginatedFetch.collect { |opts| @api.list_configs(opts) }
639
- rows.map { |r| Smplkit::Config::Helpers.config_from_json(self, ResourceShim.from_model(r)) }
640
- end
641
-
642
- private
643
-
644
- def config_body(config)
645
- SmplkitGeneratedClient::Config::ConfigResponse.new(
646
- data: SmplkitGeneratedClient::Config::ConfigResource.new(
647
- type: "config",
648
- id: config.key,
649
- attributes: SmplkitGeneratedClient::Config::Config.new(
650
- name: config.name,
651
- description: config.description,
652
- parent: config.parent_id,
653
- items: config_items_to_wire(config.items),
654
- environments: config_envs_to_wire(config.environments)
655
- )
656
- )
657
- )
658
- end
659
-
660
- def config_items_to_wire(items)
661
- return nil if items.nil? || items.empty?
662
-
663
- items.to_h do |item|
664
- [item.name, SmplkitGeneratedClient::Config::ConfigItemDefinition.new(
665
- value: item.value, type: item.type, description: item.description
666
- )]
667
- end
668
- end
669
-
670
- def config_envs_to_wire(environments)
671
- return nil if environments.empty?
672
-
673
- # Per ADR-024 §2.4 the wire shape for env overrides is a flat
674
- # +{env: {key: rawValue}}+ map — no envelope, no per-key type
675
- # wrapper. The generated +Config.environments+ attribute accepts
676
- # +Hash<String, Hash<String, Object>>+ directly.
677
- environments.each_with_object({}) do |(env_key, env_obj), out|
678
- out[env_key] = env_obj.values
679
- end
680
- end
681
-
682
- def bulk_items_to_wire(items_hash)
683
- return nil if items_hash.nil? || items_hash.empty?
684
-
685
- items_hash.transform_values do |def_hash|
686
- SmplkitGeneratedClient::Config::ConfigItemDefinition.new(
687
- value: def_hash["value"],
688
- type: def_hash["type"],
689
- description: def_hash["description"]
690
- )
691
- end
692
- end
693
-
694
- def trigger_background_flush_if_needed
695
- return unless @buffer.pending_count >= Management::CONFIG_BATCH_FLUSH_SIZE
696
-
697
- Thread.new do
698
- flush
699
- rescue StandardError => e
700
- Smplkit.debug("registration", "threshold config flush failed: #{e.class}: #{e.message}")
701
- end
702
- end
703
- end
704
-
705
- class FlagsNamespace
706
- def initialize(api_client)
707
- @api = SmplkitGeneratedClient::Flags::FlagsApi.new(api_client)
708
- @buffer = Management::FlagRegistrationBuffer.new
709
- end
710
-
711
- def register(declaration)
712
- @buffer.add(declaration)
713
- return unless @buffer.pending_count >= Management::FLAG_BATCH_FLUSH_SIZE
714
-
715
- begin
716
- flush
717
- rescue StandardError => e
718
- Smplkit.debug("registration", "threshold flag flush failed: #{e.class}: #{e.message}")
719
- end
720
- end
721
-
722
- # POST pending declarations to the bulk endpoint.
723
- #
724
- # Items remain in the buffer until the request succeeds, so a flush
725
- # against an unhealthy service is automatically retried by the next
726
- # +flush+ call (lazy +start+ retry, periodic background flush, or
727
- # final flush on close). Raises on failure — callers decide whether
728
- # to retry.
729
- def flush
730
- batch = @buffer.peek
731
- return if batch.empty?
732
-
733
- flag_items = batch.map do |entry|
734
- SmplkitGeneratedClient::Flags::FlagBulkItem.new(
735
- id: entry["id"], type: entry["type"], default: entry["default"],
736
- service: entry["service"], environment: entry["environment"]
737
- )
738
- end
739
- body = SmplkitGeneratedClient::Flags::FlagBulkRequest.new(flags: flag_items)
740
- ErrorMapping.call { @api.bulk_register_flags(body) }
741
- @buffer.commit(batch.map { |b| b["id"] })
742
- end
743
-
744
- def pending_count
745
- @buffer.pending_count
746
- end
747
-
748
- def list(page_number: nil, page_size: nil)
749
- opts = {}
750
- opts[:page_number] = page_number unless page_number.nil?
751
- opts[:page_size] = page_size unless page_size.nil?
752
- response = ErrorMapping.call { @api.list_flags(opts) }
753
- (response.data || []).map { |r| flag_from_resource(ResourceShim.from_model(r)) }
754
- end
755
-
756
- def get(id)
757
- response = ErrorMapping.call { @api.get_flag(id) }
758
- flag_from_resource(ResourceShim.from_model(response.data))
759
- end
760
-
761
- def delete(id)
762
- ErrorMapping.call { @api.delete_flag(id) }
763
- true
764
- end
765
-
766
- def new_boolean_flag(id, default:, name: nil, description: nil, values: nil)
767
- Smplkit::Flags::BooleanFlag.new(
768
- self, id: id, name: name || id, type: "BOOLEAN", default: default,
769
- description: description, values: values
770
- )
771
- end
772
-
773
- def new_string_flag(id, default:, name: nil, description: nil, values: nil)
774
- Smplkit::Flags::StringFlag.new(
775
- self, id: id, name: name || id, type: "STRING", default: default,
776
- description: description, values: values
777
- )
778
- end
779
-
780
- def new_number_flag(id, default:, name: nil, description: nil, values: nil)
781
- Smplkit::Flags::NumberFlag.new(
782
- self, id: id, name: name || id, type: "NUMERIC", default: default,
783
- description: description, values: values
784
- )
785
- end
786
-
787
- def new_json_flag(id, default:, name: nil, description: nil, values: nil)
788
- Smplkit::Flags::JsonFlag.new(
789
- self, id: id, name: name || id, type: "JSON", default: default,
790
- description: description, values: values
791
- )
792
- end
793
-
794
- def _create_flag(flag)
795
- response = ErrorMapping.call { @api.create_flag(flag_body(flag)) }
796
- flag_from_resource(ResourceShim.from_model(response.data))
797
- end
798
-
799
- def _update_flag(flag)
800
- response = ErrorMapping.call { @api.update_flag(flag.id, flag_body(flag)) }
801
- flag_from_resource(ResourceShim.from_model(response.data))
802
- end
803
-
804
- def fetch_flag(id)
805
- response = ErrorMapping.call { @api.get_flag(id) }
806
- Smplkit::Flags::Helpers.flag_dict_from_json(ResourceShim.from_model(response.data))
807
- end
808
-
809
- # Runtime entry — walks every page so an account holding more than
810
- # +RUNTIME_PAGE_SIZE+ flags still gets a complete in-memory store.
811
- def list_flags
812
- rows = PaginatedFetch.collect { |opts| @api.list_flags(opts) }
813
- rows.map { |r| Smplkit::Flags::Helpers.flag_dict_from_json(ResourceShim.from_model(r)) }
814
- end
815
-
816
- private
817
-
818
- def flag_body(flag)
819
- SmplkitGeneratedClient::Flags::FlagResponse.new(
820
- data: SmplkitGeneratedClient::Flags::FlagResource.new(
821
- type: "flag",
822
- id: flag.id,
823
- attributes: SmplkitGeneratedClient::Flags::Flag.new(
824
- name: flag.name,
825
- type: flag.type,
826
- default: flag.default,
827
- description: flag.description,
828
- values: flag_values_to_wire(flag.values),
829
- environments: flag_envs_to_wire(flag.environments)
830
- )
831
- )
832
- )
833
- end
834
-
835
- def flag_values_to_wire(values)
836
- return nil if values.nil?
837
-
838
- values.map do |v|
839
- SmplkitGeneratedClient::Flags::FlagValue.new(name: v.name, value: v.value)
840
- end
841
- end
842
-
843
- def flag_envs_to_wire(environments)
844
- return nil if environments.empty?
845
-
846
- environments.each_with_object({}) do |(env_key, env_obj), out|
847
- rules = env_obj.rules.map do |r|
848
- SmplkitGeneratedClient::Flags::FlagRule.new(
849
- logic: r.logic, value: r.value, description: r.description
850
- )
851
- end
852
- out[env_key] = SmplkitGeneratedClient::Flags::FlagEnvironment.new(
853
- enabled: env_obj.enabled, default: env_obj.default, rules: rules
854
- )
855
- end
856
- end
857
-
858
- def flag_from_resource(resource)
859
- d = Smplkit::Flags::Helpers.flag_dict_from_json(resource)
860
- klass =
861
- case d["type"]
862
- when "BOOLEAN" then Smplkit::Flags::BooleanFlag
863
- when "STRING" then Smplkit::Flags::StringFlag
864
- when "NUMERIC" then Smplkit::Flags::NumberFlag
865
- else Smplkit::Flags::JsonFlag
866
- end
867
- klass.new(
868
- self,
869
- id: d["id"], name: d["name"], type: d["type"], default: d["default"],
870
- description: d["description"], values: d["values"], environments: d["environments"],
871
- created_at: (resource["attributes"] || {})["created_at"],
872
- updated_at: (resource["attributes"] || {})["updated_at"]
873
- )
874
- end
875
- end
876
-
877
- class LoggersNamespace
878
- def initialize(api_client)
879
- @api = SmplkitGeneratedClient::Logging::LoggersApi.new(api_client)
880
- @buffer = Management::LoggerRegistrationBuffer.new
881
- end
882
-
883
- def register(source)
884
- sources = source.is_a?(Array) ? source : [source]
885
- sources.each { |s| @buffer.add(s) }
886
- flush if @buffer.pending_count >= Management::LOGGER_BATCH_FLUSH_SIZE
887
- end
888
-
889
- def flush
890
- batch = @buffer.drain
891
- return if batch.empty?
892
-
893
- items = batch.map do |entry|
894
- SmplkitGeneratedClient::Logging::LoggerBulkItem.new(
895
- id: entry["id"],
896
- resolved_level: entry["resolved_level"],
897
- level: entry["level"],
898
- service: entry["service"],
899
- environment: entry["environment"]
900
- )
901
- end
902
- body = SmplkitGeneratedClient::Logging::LoggerBulkRequest.new(loggers: items)
903
- ErrorMapping.call { @api.bulk_register_loggers(body) }
904
- rescue StandardError => e
905
- Smplkit.debug("registration", "logger flush failed: #{e.class}: #{e.message}")
906
- end
907
-
908
- def list(page_number: nil, page_size: nil)
909
- opts = {}
910
- opts[:page_number] = page_number unless page_number.nil?
911
- opts[:page_size] = page_size unless page_size.nil?
912
- response = ErrorMapping.call { @api.list_loggers(opts) }
913
- (response.data || []).map do |r|
914
- Smplkit::Logging::Helpers.logger_resource_to_model(self, ResourceShim.from_model(r))
915
- end
916
- end
917
-
918
- def get(id)
919
- normalized = Smplkit::Logging::Normalize.normalize_logger_name(id)
920
- response = ErrorMapping.call { @api.get_logger(normalized) }
921
- Smplkit::Logging::Helpers.logger_resource_to_model(self, ResourceShim.from_model(response.data))
922
- end
923
-
924
- def delete(id)
925
- normalized = Smplkit::Logging::Normalize.normalize_logger_name(id)
926
- ErrorMapping.call { @api.delete_logger(normalized) }
927
- true
928
- end
929
-
930
- def _update_logger(logger)
931
- response = ErrorMapping.call { @api.update_logger(logger.id || logger.name, logger_body(logger)) }
932
- Smplkit::Logging::Helpers.logger_resource_to_model(self, ResourceShim.from_model(response.data))
933
- end
934
-
935
- # Runtime entry — walks every page and returns an id-keyed Hash of
936
- # resolution-cache entries (+level+, +group+, +managed+,
937
- # +environments+). Mirrors the Python SDK's
938
- # +LoggingClient._fetch_and_apply+ loggers branch.
939
- def list_logger_entries
940
- rows = PaginatedFetch.collect { |opts| @api.list_loggers(opts) }
941
- rows.to_h { |r| logger_entry_from_resource(ResourceShim.from_model(r)) }
942
- end
943
-
944
- # Fetch one logger as a resolution-cache entry. Used by the
945
- # +logger_changed+ WS handler.
946
- def get_logger_entry(id)
947
- normalized = Smplkit::Logging::Normalize.normalize_logger_name(id)
948
- response = ErrorMapping.call { @api.get_logger(normalized) }
949
- logger_entry_from_resource(ResourceShim.from_model(response.data))
950
- end
951
-
952
- private
953
-
954
- def logger_entry_from_resource(resource)
955
- attrs = resource["attributes"] || {}
956
- [
957
- resource["id"],
958
- {
959
- "level" => attrs["level"],
960
- "group" => attrs["group"],
961
- "managed" => attrs.key?("managed") ? attrs["managed"] : true,
962
- "environments" => attrs["environments"] || {}
963
- }
964
- ]
965
- end
966
-
967
- def logger_body(logger)
968
- # Logger server schema: name, level, group, managed.
969
- # +resolved_level+ is read-only, +service+/+environment+ are
970
- # observed via bulk register, +description+ is wrapper-local.
971
- SmplkitGeneratedClient::Logging::LoggerResponse.new(
972
- data: SmplkitGeneratedClient::Logging::LoggerResource.new(
973
- type: "logger",
974
- id: logger.id,
975
- attributes: SmplkitGeneratedClient::Logging::Logger.new(
976
- name: logger.name,
977
- level: logger.level&.to_s,
978
- group: logger.log_group_id,
979
- managed: logger.managed
980
- )
981
- )
982
- )
983
- end
984
- end
985
-
986
- class LogGroupsNamespace
987
- def initialize(api_client)
988
- @api = SmplkitGeneratedClient::Logging::LogGroupsApi.new(api_client)
989
- end
990
-
991
- def list(page_number: nil, page_size: nil)
992
- opts = {}
993
- opts[:page_number] = page_number unless page_number.nil?
994
- opts[:page_size] = page_size unless page_size.nil?
995
- response = ErrorMapping.call { @api.list_log_groups(opts) }
996
- (response.data || []).map do |r|
997
- Smplkit::Logging::Helpers.log_group_resource_to_model(self, ResourceShim.from_model(r))
998
- end
999
- end
1000
-
1001
- def get(key)
1002
- response = ErrorMapping.call { @api.get_log_group(key) }
1003
- Smplkit::Logging::Helpers.log_group_resource_to_model(self, ResourceShim.from_model(response.data))
1004
- end
1005
-
1006
- def delete(key)
1007
- ErrorMapping.call { @api.delete_log_group(key) }
1008
- true
1009
- end
1010
-
1011
- def new_log_group(key, name: nil, level: nil, description: nil, parent: nil)
1012
- Smplkit::Logging::SmplLogGroup.new(
1013
- self, key: key, name: name || Smplkit::Helpers.key_to_display_name(key),
1014
- level: level && Smplkit::LogLevel.coerce(level), description: description,
1015
- parent_id: parent.is_a?(Smplkit::Logging::SmplLogGroup) ? parent.key : parent
1016
- )
1017
- end
1018
-
1019
- def _create_log_group(group)
1020
- response = ErrorMapping.call { @api.create_log_group(log_group_body(group)) }
1021
- Smplkit::Logging::Helpers.log_group_resource_to_model(self, ResourceShim.from_model(response.data))
1022
- end
1023
-
1024
- def _update_log_group(group)
1025
- response = ErrorMapping.call { @api.update_log_group(group.key, log_group_body(group)) }
1026
- Smplkit::Logging::Helpers.log_group_resource_to_model(self, ResourceShim.from_model(response.data))
1027
- end
1028
-
1029
- # Runtime entry — walks every page and returns an id-keyed Hash of
1030
- # resolution-cache entries (+level+, +group+, +environments+). The
1031
- # +group+ key carries the *parent group id* so the resolution
1032
- # algorithm can walk the chain with the same key shape it uses for
1033
- # loggers.
1034
- def list_group_entries
1035
- rows = PaginatedFetch.collect { |opts| @api.list_log_groups(opts) }
1036
- rows.to_h { |r| group_entry_from_resource(ResourceShim.from_model(r)) }
1037
- end
1038
-
1039
- def get_group_entry(key)
1040
- response = ErrorMapping.call { @api.get_log_group(key) }
1041
- group_entry_from_resource(ResourceShim.from_model(response.data))
1042
- end
1043
-
1044
- private
1045
-
1046
- def group_entry_from_resource(resource)
1047
- attrs = resource["attributes"] || {}
1048
- [
1049
- resource["id"],
1050
- {
1051
- "level" => attrs["level"],
1052
- "group" => attrs["parent_id"],
1053
- "environments" => attrs["environments"] || {}
1054
- }
1055
- ]
1056
- end
1057
-
1058
- def log_group_body(group)
1059
- # LogGroup server schema: name, level, parent_id (no description).
1060
- SmplkitGeneratedClient::Logging::LogGroupResponse.new(
1061
- data: SmplkitGeneratedClient::Logging::LogGroupResource.new(
1062
- type: "log_group",
1063
- id: group.key,
1064
- attributes: SmplkitGeneratedClient::Logging::LogGroup.new(
1065
- name: group.name,
1066
- level: group.level&.to_s,
1067
- parent_id: group.parent_id
1068
- )
1069
- )
1070
- )
1071
- end
1072
- end
1073
- end
1074
- end