smplkit 1.0.11 → 1.0.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45b89bb8091aeb0e42f1e6ccbf0b94cde98f827bae408770317658dd24c4de3c
4
- data.tar.gz: 06eff16b91b28ef98f9fc2b7a64253d099154a6730a7ecbf1f2e99df9d8629c3
3
+ metadata.gz: 0f74b529f57291b182d674d166e138a61b95bb01c1bdf01a71261405f75a6879
4
+ data.tar.gz: 7e08e3ec63e892ae13bec37446afcd38c7b5d16008a0396d7046efd362598ed9
5
5
  SHA512:
6
- metadata.gz: 8f06ccbadf581ad0f7253ebb80ca35ecc6f1dcd94c63ad476bbfd742068702774f9b2dc416da6d6f035fd50502cebea17bfae6d8a203f4d0f4c39dec495c30ef
7
- data.tar.gz: 6ac31365d9a694b9b11e0a8b00e7ca37c3a8fa5354fe678ef91fdc7bcd7a3ad257220b5521a4704ebdb210838c3e4ad120afe5dc4af01686f7339538510b0024
6
+ metadata.gz: 28aa21a7b049e824c613ab8d53df09a0993abd938cb275ee7a330735623b9e6de99b986f6930bc6bab472a26533abf472d5a6f41301e54d833856fdb6708c882
7
+ data.tar.gz: 738683d247b8a2c557731deaae62c93120899da054dcfb91991d7ee114dd5a40c7f945456916713aa4500aa2a6e1fefe5ea0916f682804c63a4eea2af3ad1afc
data/README.md CHANGED
@@ -77,7 +77,14 @@ Two adapters ship at launch (per ADR-046 §2.3):
77
77
  | `stdlib-logger` | Ruby stdlib `Logger` (and Rails via `ActiveSupport::Logger`) |
78
78
  | `semantic-logger` | The [`semantic_logger` gem](https://github.com/reidmorrison/semantic_logger) |
79
79
 
80
- Custom adapters subclass `Smplkit::Logging::Adapters::Base`.
80
+ Both are auto-loaded by `install` when the corresponding framework is available. To support an additional framework, subclass `Smplkit::Logging::Adapters::Base` and implement the five contract methods (`name`, `discover`, `apply_level`, `install_hook`, `uninstall_hook`), then register before `install`:
81
+
82
+ ```ruby
83
+ client.logging.register_adapter(MyFrameworkAdapter.new)
84
+ client.logging.install
85
+ ```
86
+
87
+ Calling `register_adapter` disables auto-loading — only the adapters you register are used.
81
88
 
82
89
  ## Rails integration
83
90
 
@@ -157,11 +157,21 @@ module Smplkit
157
157
 
158
158
  def typed_get(item_key, default, config_key)
159
159
  snapshot = config_key ? resolve(config_key) : merged_snapshot
160
- keys = item_key.to_s.split(".")
161
- value = keys.reduce(snapshot) do |scope, k|
162
- break default unless scope.is_a?(Hash)
163
-
164
- scope[k]
160
+ key = item_key.to_s
161
+ # Items live under flat dotted keys (e.g. +"api.host"+ not nested
162
+ # +api host+). Match Python's +current_values.get(key)+ behavior.
163
+ value = snapshot[key]
164
+ if value.nil? && snapshot.is_a?(Hash)
165
+ # Fallback: support callers that constructed an explicitly-nested
166
+ # snapshot (rare — typed model bindings only).
167
+ parts = key.split(".")
168
+ if parts.length > 1
169
+ value = parts.reduce(snapshot) do |scope, k|
170
+ break nil unless scope.is_a?(Hash)
171
+
172
+ scope[k]
173
+ end
174
+ end
165
175
  end
166
176
  return default if value.nil?
167
177
 
@@ -32,7 +32,7 @@ module Smplkit
32
32
  key: attrs["key"] || resource["id"],
33
33
  name: attrs["name"],
34
34
  description: attrs["description"],
35
- parent_id: attrs["parent_id"],
35
+ parent_id: attrs["parent"] || attrs["parent_id"],
36
36
  items: items,
37
37
  environments: environments,
38
38
  created_at: attrs["created_at"],
@@ -54,11 +54,12 @@ module Smplkit
54
54
  out[env] = { "values" => env_obj.values_raw }
55
55
  end
56
56
 
57
+ # The Config schema (per the OpenAPI spec) does not include +key+ in
58
+ # attributes — the resource +id+ carries the customer-facing key.
57
59
  attributes = {
58
- "key" => config.key,
59
60
  "name" => config.name,
60
61
  "description" => config.description,
61
- "parent_id" => config.parent_id,
62
+ "parent" => config.parent_id,
62
63
  "items" => items,
63
64
  "environments" => environments
64
65
  }.compact
@@ -81,7 +81,10 @@ module Smplkit
81
81
 
82
82
  sections =
83
83
  begin
84
- parse_ini(File.read(path))
84
+ # Force UTF-8 — the file may contain comments with non-ASCII bytes
85
+ # (em dashes, smart quotes) and the system's default external
86
+ # encoding is locale-dependent.
87
+ parse_ini(File.read(path, encoding: "UTF-8"))
85
88
  rescue StandardError
86
89
  return {}
87
90
  end
@@ -143,25 +143,25 @@ module Smplkit
143
143
  return if batch.empty?
144
144
 
145
145
  body = { "data" => { "type" => "context_bulk_register", "attributes" => { "contexts" => batch } } }
146
- http_post("/api/contexts/v1/bulk", body)
146
+ http_post("/api/v1/contexts/bulk", body)
147
147
  rescue StandardError => e
148
148
  Smplkit.debug("registration", "context flush failed: #{e.class}: #{e.message}")
149
149
  end
150
150
 
151
151
  def list
152
- list_resp = http_list("/api/contexts/v1")
152
+ list_resp = http_list("/api/v1/contexts")
153
153
  list_resp.map { |r| context_from_resource(r) }
154
154
  end
155
155
 
156
156
  def get(id_or_type, key = nil)
157
157
  type, ckey = split_id(id_or_type, key)
158
- resource = http_get("/api/contexts/v1/#{type}:#{ckey}")
158
+ resource = http_get("/api/v1/contexts/#{type}:#{ckey}")
159
159
  context_from_resource(resource["data"])
160
160
  end
161
161
 
162
162
  def delete(id_or_type, key = nil)
163
163
  type, ckey = split_id(id_or_type, key)
164
- http_delete("/api/contexts/v1/#{type}:#{ckey}")
164
+ http_delete("/api/v1/contexts/#{type}:#{ckey}")
165
165
  end
166
166
 
167
167
  def _save_context(ctx)
@@ -172,7 +172,7 @@ module Smplkit
172
172
  "attributes" => { "type" => ctx.type, "key" => ctx.key, "attributes" => ctx.attributes }.compact
173
173
  }
174
174
  }
175
- resp = http_put("/api/contexts/v1/#{ctx.id}", body)
175
+ resp = http_put("/api/v1/contexts/#{ctx.id}", body)
176
176
  context_from_resource(resp["data"]).tap { |c| c._bind_client(self) }
177
177
  end
178
178
 
@@ -210,17 +210,17 @@ module Smplkit
210
210
  end
211
211
 
212
212
  def list
213
- list_resp = http_list("/api/context_types/v1")
213
+ list_resp = http_list("/api/v1/context_types")
214
214
  list_resp.map { |r| from_resource(r) }
215
215
  end
216
216
 
217
217
  def get(key)
218
- resp = http_get("/api/context_types/v1/#{key}")
218
+ resp = http_get("/api/v1/context_types/#{key}")
219
219
  from_resource(resp["data"])
220
220
  end
221
221
 
222
222
  def delete(key)
223
- http_delete("/api/context_types/v1/#{key}")
223
+ http_delete("/api/v1/context_types/#{key}")
224
224
  end
225
225
 
226
226
  def new_context_type(key, name: nil, description: nil)
@@ -228,12 +228,12 @@ module Smplkit
228
228
  end
229
229
 
230
230
  def _create_context_type(ct)
231
- resp = http_post("/api/context_types/v1", body_for(ct))
231
+ resp = http_post("/api/v1/context_types", body_for(ct))
232
232
  from_resource(resp["data"])
233
233
  end
234
234
 
235
235
  def _update_context_type(ct)
236
- resp = http_put("/api/context_types/v1/#{ct.key}", body_for(ct))
236
+ resp = http_put("/api/v1/context_types/#{ct.key}", body_for(ct))
237
237
  from_resource(resp["data"])
238
238
  end
239
239
 
@@ -268,17 +268,17 @@ module Smplkit
268
268
  end
269
269
 
270
270
  def list
271
- list_resp = http_list("/api/environments/v1")
271
+ list_resp = http_list("/api/v1/environments")
272
272
  list_resp.map { |r| from_resource(r) }
273
273
  end
274
274
 
275
275
  def get(key)
276
- resp = http_get("/api/environments/v1/#{key}")
276
+ resp = http_get("/api/v1/environments/#{key}")
277
277
  from_resource(resp["data"])
278
278
  end
279
279
 
280
280
  def delete(key)
281
- http_delete("/api/environments/v1/#{key}")
281
+ http_delete("/api/v1/environments/#{key}")
282
282
  end
283
283
 
284
284
  def new(key, name: nil, color: nil,
@@ -293,12 +293,12 @@ module Smplkit
293
293
  end
294
294
 
295
295
  def _create_environment(env)
296
- resp = http_post("/api/environments/v1", body_for(env))
296
+ resp = http_post("/api/v1/environments", body_for(env))
297
297
  from_resource(resp["data"])
298
298
  end
299
299
 
300
300
  def _update_environment(env)
301
- resp = http_put("/api/environments/v1/#{env.key}", body_for(env))
301
+ resp = http_put("/api/v1/environments/#{env.key}", body_for(env))
302
302
  from_resource(resp["data"])
303
303
  end
304
304
 
@@ -342,12 +342,12 @@ module Smplkit
342
342
  end
343
343
 
344
344
  def get
345
- resp = http_get("/api/account_settings/v1")
345
+ resp = http_get("/api/v1/accounts/current/settings")
346
346
  from_resource(resp["data"])
347
347
  end
348
348
 
349
349
  def _update_account_settings(settings)
350
- resp = http_put("/api/account_settings/v1", body_for(settings))
350
+ resp = http_put("/api/v1/accounts/current/settings", body_for(settings))
351
351
  from_resource(resp["data"])
352
352
  end
353
353
 
@@ -385,41 +385,86 @@ module Smplkit
385
385
  end
386
386
 
387
387
  def list
388
- list_resp = http_list("/api/configs/v1")
388
+ list_resp = http_list("/api/v1/configs")
389
389
  list_resp.map { |r| Smplkit::Config::Helpers.config_from_json(self, r) }
390
390
  end
391
391
 
392
392
  def get(key)
393
- resp = http_get("/api/configs/v1/#{key}")
393
+ resp = http_get("/api/v1/configs/#{key}")
394
394
  Smplkit::Config::Helpers.config_from_json(self, resp["data"])
395
395
  end
396
396
 
397
397
  def delete(key)
398
- http_delete("/api/configs/v1/#{key}")
398
+ http_delete("/api/v1/configs/#{key}")
399
399
  end
400
400
 
401
401
  def new_config(key, name: nil, description: nil, parent: nil)
402
402
  Smplkit::Config::Config.new(
403
- self, key: key, name: name, description: description,
404
- parent_id: parent.is_a?(Smplkit::Config::Config) ? parent.key : parent
403
+ self,
404
+ key: key,
405
+ name: name || Smplkit::Helpers.key_to_display_name(key),
406
+ description: description,
407
+ parent_id: parent.is_a?(Smplkit::Config::Config) ? parent.key : parent
405
408
  )
406
409
  end
407
410
 
408
411
  def _create_config(config)
409
412
  body = Smplkit::Config::Helpers.build_config_request_body(config)
410
- resp = http_post("/api/configs/v1", body)
413
+ resp = http_post("/api/v1/configs", body)
411
414
  Smplkit::Config::Helpers.config_from_json(self, resp["data"])
412
415
  end
413
416
 
414
417
  def _update_config(config)
415
418
  body = Smplkit::Config::Helpers.build_config_request_body(config)
416
- resp = http_put("/api/configs/v1/#{config.key}", body)
419
+ resp = http_put("/api/v1/configs/#{config.key}", body)
417
420
  Smplkit::Config::Helpers.config_from_json(self, resp["data"])
418
421
  end
419
422
 
420
- def fetch_chain(key)
421
- resp = http_get("/api/configs/v1/#{key}/chain")
422
- (resp["data"] || []).map { |r| r["attributes"] || {} }
423
+ # Build the parent-chain for a given config, walking +parent_id+
424
+ # pointers across the full config list. Mirrors the Python SDK's
425
+ # client-side resolution there is no server +/chain+ endpoint.
426
+ #
427
+ # Each chain entry is a Hash matching the wire shape that
428
+ # +Smplkit::Config::Helpers.resolve_chain+ consumes.
429
+ def fetch_chain(target_key)
430
+ all_configs = list
431
+ by_key = all_configs.to_h { |c| [c.key, c] }
432
+ by_id = all_configs.to_h { |c| [c.id, c] }
433
+
434
+ current = by_key[target_key]
435
+ return [] unless current
436
+
437
+ chain = []
438
+ loop do
439
+ chain << config_to_chain_entry(current)
440
+ parent_id = current.parent_id
441
+ break if parent_id.nil? || parent_id == ""
442
+
443
+ parent = by_id[parent_id]
444
+ break unless parent
445
+
446
+ current = parent
447
+ end
448
+ chain
449
+ end
450
+
451
+ private
452
+
453
+ def config_to_chain_entry(config)
454
+ items_hash = {}
455
+ config.items.each do |item|
456
+ items_hash[item.name] = {
457
+ "value" => item.value,
458
+ "type" => item.type,
459
+ "description" => item.description
460
+ }.compact
461
+ end
462
+
463
+ environments = config.environments.each_with_object({}) do |(env_key, env_obj), out|
464
+ out[env_key] = { "values" => env_obj.values_raw }
465
+ end
466
+
467
+ { "id" => config.id, "items" => items_hash, "environments" => environments }
423
468
  end
424
469
  end
425
470
 
@@ -441,23 +486,23 @@ module Smplkit
441
486
  return if batch.empty?
442
487
 
443
488
  body = { "data" => { "type" => "flag_bulk_register", "attributes" => { "flags" => batch } } }
444
- http_post("/api/flags/v1/bulk", body)
489
+ http_post("/api/v1/flags/bulk", body)
445
490
  rescue StandardError => e
446
491
  Smplkit.debug("registration", "flag flush failed: #{e.class}: #{e.message}")
447
492
  end
448
493
 
449
494
  def list
450
- list_resp = http_list("/api/flags/v1")
495
+ list_resp = http_list("/api/v1/flags")
451
496
  list_resp.map { |r| flag_from_resource(r) }
452
497
  end
453
498
 
454
499
  def get(id)
455
- resp = http_get("/api/flags/v1/#{id}")
500
+ resp = http_get("/api/v1/flags/#{id}")
456
501
  flag_from_resource(resp["data"])
457
502
  end
458
503
 
459
504
  def delete(id)
460
- http_delete("/api/flags/v1/#{id}")
505
+ http_delete("/api/v1/flags/#{id}")
461
506
  end
462
507
 
463
508
  def new_boolean_flag(id, default:, name: nil, description: nil, values: nil)
@@ -490,23 +535,23 @@ module Smplkit
490
535
 
491
536
  def _create_flag(flag)
492
537
  body = Smplkit::Flags::Helpers.build_flag_request_body(flag)
493
- resp = http_post("/api/flags/v1", body)
538
+ resp = http_post("/api/v1/flags", body)
494
539
  flag_from_resource(resp["data"])
495
540
  end
496
541
 
497
542
  def _update_flag(flag)
498
543
  body = Smplkit::Flags::Helpers.build_flag_request_body(flag)
499
- resp = http_put("/api/flags/v1/#{flag.id}", body)
544
+ resp = http_put("/api/v1/flags/#{flag.id}", body)
500
545
  flag_from_resource(resp["data"])
501
546
  end
502
547
 
503
548
  def fetch_flag(id)
504
- resp = http_get("/api/flags/v1/#{id}")
549
+ resp = http_get("/api/v1/flags/#{id}")
505
550
  Smplkit::Flags::Helpers.flag_dict_from_json(resp["data"])
506
551
  end
507
552
 
508
553
  def list_flags
509
- body = http_list("/api/flags/v1")
554
+ body = http_list("/api/v1/flags")
510
555
  body.map { |r| Smplkit::Flags::Helpers.flag_dict_from_json(r) }
511
556
  end
512
557
 
@@ -550,30 +595,30 @@ module Smplkit
550
595
  return if batch.empty?
551
596
 
552
597
  body = { "data" => { "type" => "logger_bulk_register", "attributes" => { "loggers" => batch } } }
553
- http_post("/api/loggers/v1/bulk", body)
598
+ http_post("/api/v1/loggers/bulk", body)
554
599
  rescue StandardError => e
555
600
  Smplkit.debug("registration", "logger flush failed: #{e.class}: #{e.message}")
556
601
  end
557
602
 
558
603
  def list
559
- list_resp = http_list("/api/loggers/v1")
604
+ list_resp = http_list("/api/v1/loggers")
560
605
  list_resp.map { |r| Smplkit::Logging::Helpers.logger_resource_to_model(self, r) }
561
606
  end
562
607
 
563
608
  def get(id)
564
609
  normalized = Smplkit::Logging::Normalize.normalize_logger_name(id)
565
- resp = http_get("/api/loggers/v1/#{normalized}")
610
+ resp = http_get("/api/v1/loggers/#{normalized}")
566
611
  Smplkit::Logging::Helpers.logger_resource_to_model(self, resp["data"])
567
612
  end
568
613
 
569
614
  def delete(id)
570
615
  normalized = Smplkit::Logging::Normalize.normalize_logger_name(id)
571
- http_delete("/api/loggers/v1/#{normalized}")
616
+ http_delete("/api/v1/loggers/#{normalized}")
572
617
  end
573
618
 
574
619
  def _update_logger(logger)
575
620
  body = Smplkit::Logging::Helpers.build_logger_body(logger)
576
- resp = http_put("/api/loggers/v1/#{logger.id || logger.name}", body)
621
+ resp = http_put("/api/v1/loggers/#{logger.id || logger.name}", body)
577
622
  Smplkit::Logging::Helpers.logger_resource_to_model(self, resp["data"])
578
623
  end
579
624
  end
@@ -586,17 +631,17 @@ module Smplkit
586
631
  end
587
632
 
588
633
  def list
589
- list_resp = http_list("/api/log_groups/v1")
634
+ list_resp = http_list("/api/v1/log_groups")
590
635
  list_resp.map { |r| Smplkit::Logging::Helpers.log_group_resource_to_model(self, r) }
591
636
  end
592
637
 
593
638
  def get(key)
594
- resp = http_get("/api/log_groups/v1/#{key}")
639
+ resp = http_get("/api/v1/log_groups/#{key}")
595
640
  Smplkit::Logging::Helpers.log_group_resource_to_model(self, resp["data"])
596
641
  end
597
642
 
598
643
  def delete(key)
599
- http_delete("/api/log_groups/v1/#{key}")
644
+ http_delete("/api/v1/log_groups/#{key}")
600
645
  end
601
646
 
602
647
  def new_log_group(key, name: nil, level: nil, description: nil, parent: nil)
@@ -609,13 +654,13 @@ module Smplkit
609
654
 
610
655
  def _create_log_group(group)
611
656
  body = Smplkit::Logging::Helpers.build_log_group_body(group)
612
- resp = http_post("/api/log_groups/v1", body)
657
+ resp = http_post("/api/v1/log_groups", body)
613
658
  Smplkit::Logging::Helpers.log_group_resource_to_model(self, resp["data"])
614
659
  end
615
660
 
616
661
  def _update_log_group(group)
617
662
  body = Smplkit::Logging::Helpers.build_log_group_body(group)
618
- resp = http_put("/api/log_groups/v1/#{group.key}", body)
663
+ resp = http_put("/api/v1/log_groups/#{group.key}", body)
619
664
  Smplkit::Logging::Helpers.log_group_resource_to_model(self, resp["data"])
620
665
  end
621
666
  end
data/lib/smplkit/ws.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "async"
5
5
  require "async/http/endpoint"
6
+ require "async/http/protocol/http1"
6
7
  require "async/websocket/client"
7
8
  require "concurrent"
8
9
 
@@ -172,7 +173,12 @@ module Smplkit
172
173
  @connection_status = "connecting"
173
174
  Smplkit.debug("websocket", "connecting to #{safe_url}")
174
175
 
175
- endpoint = Async::HTTP::Endpoint.parse(url)
176
+ # Force HTTP/1.1 for the WebSocket upgrade. async-http defaults to
177
+ # HTTP/2 on TLS endpoints, but the smplkit event gateway speaks the
178
+ # classic RFC 6455 upgrade over HTTP/1.1, not the HTTP/2-tunneled
179
+ # variant from RFC 8441 — without this override the upgrade returns
180
+ # a Protocol::HTTP2::StreamError before any frame is exchanged.
181
+ endpoint = Async::HTTP::Endpoint.parse(url, protocol: Async::HTTP::Protocol::HTTP1)
176
182
  headers = { "user-agent" => USER_AGENT }
177
183
  connection = Async::WebSocket::Client.connect(endpoint, headers: headers)
178
184
  @connection_lock.synchronize { @connection = connection }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smplkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.11
4
+ version: 1.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Smpl Solutions LLC