quonfig 2.0.0 → 2.1.0
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/CHANGELOG.md +5 -0
- data/README.md +20 -3
- data/lib/quonfig/bound_client.rb +5 -0
- data/lib/quonfig/client.rb +54 -15
- data/lib/quonfig/evaluation_details.rb +11 -4
- data/lib/quonfig/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 939842e745249a0ce6cee689801a6fe3a7cbdc56552ad88bdabaa0c36e79ef92
|
|
4
|
+
data.tar.gz: df5b171629c4b6c556f04b80a0035d5726c76929e78d11ba5382332d0e4b2293
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c6822328fa6147d3816b404fe9543991d7095a5f663a2dc045f425113daba93f18da02e739c32dfce962858b00c190809acf4a62e56f1f049669bf813d8cdcd
|
|
7
|
+
data.tar.gz: 04a636c09573823cf0895c3cbbdcd2d9d79f22b875a877e55587244a2077bf1ae054007b729cc64bb8a2208e806fbdb962a7c55be38f2663c015b41d2685e09d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0 - 2026-10-29
|
|
4
|
+
|
|
5
|
+
- **Added: `evaluate_details(key, context:)`, an untyped details read (qfg-177f).** It returns a `Quonfig::EvaluationDetails` whose `value` is the config's resolved value as stored, after ENV_VAR provision and decryption, with no type check or conversion: a duration comes back as its ISO 8601 text exactly as stored (`"PT90S"`, not milliseconds), a `string_list` as an Array, JSON as stored. The new `value_type` attribute names the stored type (`"duration"`, `"int"`, ...; `nil` when nothing resolved). Reason, variant, `flag_metadata`, error codes and telemetry are the same as the typed `*_details` getters, and it never raises. A malformed stored value (a duration `"30s"`) is returned as-is with a success reason; an unset or unparseable ENV_VAR is still `reason: "ERROR"`, as in the typed getters. Available on the client and on a `with_context` bound client. It matches sdk-go's `Client.EvaluateDetails` and is meant for OpenFeature providers and other callers that do their own typing. Existing getters are unchanged. Tests: `test/test_evaluate_details.rb`.
|
|
6
|
+
- **Fixed: `connection_state` no longer answers `:connected` before the SSE stream's first frame (qfg-fi1s).** On a network client with SSE on, the init fetch installs an envelope before the stream has delivered anything, and `connection_state` read that install as `:connected`. It now reports `:initializing` until the stream's first frame arrives (the connected edge), as the supervisor contract says ("after 200 headers, no frame" is initializing) and as sdk-go, sdk-java, sdk-net and sdk-python already do. Datadir clients and clients with `enable_sse: false` still read `:connected` once config is installed. Tests: `test/test_health_primitives.rb`, `test/test_sse_first_frame.rb` (Test 9a/9b now assert on the public getter).
|
|
7
|
+
|
|
3
8
|
## 2.0.0 - 2026-10-26
|
|
4
9
|
|
|
5
10
|
The typed-getter contract (qfg-zvyy), shared by every Quonfig SDK and pinned by
|
data/README.md
CHANGED
|
@@ -484,6 +484,21 @@ raise. They return `Quonfig::EvaluationDetails`; a wrong-type or malformed
|
|
|
484
484
|
value is `reason: "ERROR"` with `error_code: "TYPE_MISMATCH"`.
|
|
485
485
|
`get_duration_details` carries the duration in integer milliseconds.
|
|
486
486
|
|
|
487
|
+
To read a value without a type, use `evaluate_details(key, context:)`. It
|
|
488
|
+
returns the value as stored, with `value_type` naming its type, and the same
|
|
489
|
+
`reason`, `variant` and `flag_metadata` as the typed details getters. A
|
|
490
|
+
duration comes back as its ISO 8601 text:
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
details = client.evaluate_details('http.timeout', context: { user: { key: 'u1' } })
|
|
494
|
+
details.value # => "PT90S"
|
|
495
|
+
details.value_type # => "duration"
|
|
496
|
+
details.reason # => "STATIC"
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
It does no type check and never raises; a missing key is `reason: "ERROR"`
|
|
500
|
+
with `error_code: "FLAG_NOT_FOUND"`.
|
|
501
|
+
|
|
487
502
|
## Dynamic log levels (SemanticLogger)
|
|
488
503
|
|
|
489
504
|
Quonfig can drive per-class log levels at runtime. Set config keys like
|
|
@@ -607,8 +622,8 @@ Caveats:
|
|
|
607
622
|
threads race the first request.
|
|
608
623
|
- **`connection_state` never triggers the re-initialization** — a diagnostic
|
|
609
624
|
must not open a socket. A child that has not used the client yet answers
|
|
610
|
-
`:initializing`, which is exactly what it is; it flips to `:connected`
|
|
611
|
-
first use.
|
|
625
|
+
`:initializing`, which is exactly what it is; it flips to `:connected`
|
|
626
|
+
after its first use, once the stream delivers its first frame.
|
|
612
627
|
- The child's telemetry aggregators start empty. The parent flushes the data
|
|
613
628
|
it collected before the fork; the child reports only its own.
|
|
614
629
|
- **Per-job forking pays per job.** A Resque-style worker that forks a child
|
|
@@ -757,7 +772,9 @@ reuse the parent's client object in a child process without it.
|
|
|
757
772
|
envelope install (any source: datadir, initial HTTP fetch, SSE, or fallback
|
|
758
773
|
polling). Returns `nil` before the first install. Preserved across `stop`.
|
|
759
774
|
- `client.connection_state` — a `Symbol` describing the aggregate state:
|
|
760
|
-
`:initializing`, `:connected`, `:disconnected`, or `:falling_back`.
|
|
775
|
+
`:initializing`, `:connected`, `:disconnected`, or `:falling_back`. With
|
|
776
|
+
SSE on, the client reads `:initializing` until the stream delivers its
|
|
777
|
+
first frame, even after the initial fetch has installed config.
|
|
761
778
|
- `client.callback_errors_total(callback: 'on_config_update')` — how many
|
|
762
779
|
times your `on_update` block raised. The SDK catches the error, logs it at
|
|
763
780
|
ERROR with its stack, and keeps delivering updates; a raise is not a worker
|
data/lib/quonfig/bound_client.rb
CHANGED
|
@@ -74,6 +74,11 @@ module Quonfig
|
|
|
74
74
|
@client.get_duration_details(key, context: @context)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# See Quonfig::Client#evaluate_details.
|
|
78
|
+
def evaluate_details(key)
|
|
79
|
+
@client.evaluate_details(key, context: @context)
|
|
80
|
+
end
|
|
81
|
+
|
|
77
82
|
def enabled?(feature_name)
|
|
78
83
|
@client.enabled?(feature_name, @context)
|
|
79
84
|
end
|
data/lib/quonfig/client.rb
CHANGED
|
@@ -285,34 +285,48 @@ module Quonfig
|
|
|
285
285
|
# raise — exceptions are caught and rendered as ERROR details.
|
|
286
286
|
|
|
287
287
|
def get_bool_details(key, context: NO_DEFAULT_PROVIDED)
|
|
288
|
-
|
|
288
|
+
build_details(key, :bool, context)
|
|
289
289
|
end
|
|
290
290
|
|
|
291
291
|
def get_string_details(key, context: NO_DEFAULT_PROVIDED)
|
|
292
|
-
|
|
292
|
+
build_details(key, String, context)
|
|
293
293
|
end
|
|
294
294
|
|
|
295
295
|
def get_int_details(key, context: NO_DEFAULT_PROVIDED)
|
|
296
|
-
|
|
296
|
+
build_details(key, Integer, context)
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
def get_float_details(key, context: NO_DEFAULT_PROVIDED)
|
|
300
|
-
|
|
300
|
+
build_details(key, Float, context)
|
|
301
301
|
end
|
|
302
302
|
|
|
303
303
|
def get_string_list_details(key, context: NO_DEFAULT_PROVIDED)
|
|
304
|
-
|
|
304
|
+
build_details(key, :string_list, context)
|
|
305
305
|
end
|
|
306
306
|
|
|
307
307
|
def get_json_details(key, context: NO_DEFAULT_PROVIDED)
|
|
308
|
-
|
|
308
|
+
build_details(key, :json, context)
|
|
309
309
|
end
|
|
310
310
|
|
|
311
311
|
# Integer milliseconds in +value+ (qfg-2agi.27). A config whose valueType
|
|
312
312
|
# is not duration is ERROR / TYPE_MISMATCH; a malformed value (stored or
|
|
313
313
|
# ENV_VAR) is ERROR with a nil value.
|
|
314
314
|
def get_duration_details(key, context: NO_DEFAULT_PROVIDED)
|
|
315
|
-
|
|
315
|
+
build_details(key, :duration, context)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Untyped details read (matches sdk-go Client.EvaluateDetails). +value+
|
|
319
|
+
# is the resolved value in its stored form, after ENV_VAR provision and
|
|
320
|
+
# decryption, with no type check, coercion or parse validation: a
|
|
321
|
+
# duration is its ISO 8601 text exactly as stored ("PT90S"), a
|
|
322
|
+
# string_list an Array, a json value as stored. +value_type+ is the
|
|
323
|
+
# stored value type ('string', 'int', 'double', 'bool', 'string_list',
|
|
324
|
+
# 'duration', 'json', ...; nil when nothing resolved). Reason, variant,
|
|
325
|
+
# error_code and telemetry are the same as the typed *_details getters,
|
|
326
|
+
# and like them it never raises. Meant for OpenFeature providers and
|
|
327
|
+
# other callers that do their own typing.
|
|
328
|
+
def evaluate_details(key, context: NO_DEFAULT_PROVIDED)
|
|
329
|
+
build_details(key, nil, context)
|
|
316
330
|
end
|
|
317
331
|
|
|
318
332
|
# True only when the flag resolves to the bool +true+. Never raises
|
|
@@ -650,7 +664,8 @@ module Quonfig
|
|
|
650
664
|
# Aggregate connection state. Returns one of:
|
|
651
665
|
#
|
|
652
666
|
# - +:initializing+ — no envelope has been installed and SSE is not yet
|
|
653
|
-
# connected
|
|
667
|
+
# connected, or (SSE clients) the stream has not delivered its first
|
|
668
|
+
# frame yet, even if the init fetch already installed an envelope.
|
|
654
669
|
# - +:connected+ — SSE is live, or the SDK is delivering configs from a
|
|
655
670
|
# loaded envelope (datadir mode or post-initial-fetch with no SSE).
|
|
656
671
|
# - +:disconnected+ — +stop+ was called, or SSE errored and no fallback
|
|
@@ -681,9 +696,14 @@ module Quonfig
|
|
|
681
696
|
next :disconnected if sse_channel_expected? && !sse_worker_alive?
|
|
682
697
|
next :connected if @sse_state == :connected
|
|
683
698
|
next :disconnected if @sse_state == :error
|
|
699
|
+
# First-frame rule (qfg-fi1s): a client that is supposed to hold an
|
|
700
|
+
# SSE stream is not :connected until that stream delivers its first
|
|
701
|
+
# frame. The init fetch's install alone does not make it connected;
|
|
702
|
+
# "after 200 headers, no frame" is :initializing (contract Test 6).
|
|
703
|
+
next :initializing if sse_channel_expected? && !@sse_ever_connected
|
|
684
704
|
|
|
685
705
|
# No SSE state change yet: state is driven by whether any envelope
|
|
686
|
-
# has been installed (datadir / initial fetch).
|
|
706
|
+
# has been installed (datadir / SSE disabled / initial fetch).
|
|
687
707
|
@last_successful_refresh.nil? ? :initializing : :connected
|
|
688
708
|
end
|
|
689
709
|
end
|
|
@@ -958,7 +978,7 @@ module Quonfig
|
|
|
958
978
|
end
|
|
959
979
|
|
|
960
980
|
# Lazy post-fork re-initialization. Called from every read entry point
|
|
961
|
-
# (+get+, +evaluate_details
|
|
981
|
+
# (+get+, +evaluate_details+ / the +*_details+ getters, +defined?+, +keys+) — the flag read is a
|
|
962
982
|
# plain boolean, so the steady-state cost is one comparison per lookup.
|
|
963
983
|
#
|
|
964
984
|
# The first caller in the child does what +Client.new+ does: its own
|
|
@@ -2185,9 +2205,11 @@ module Quonfig
|
|
|
2185
2205
|
end
|
|
2186
2206
|
|
|
2187
2207
|
# Build a Quonfig::EvaluationDetails for +key+, evaluated against the
|
|
2188
|
-
# caller's context, after coercing/checking +expected_type+.
|
|
2189
|
-
#
|
|
2190
|
-
|
|
2208
|
+
# caller's context, after coercing/checking +expected_type+. A nil
|
|
2209
|
+
# +expected_type+ is the untyped #evaluate_details read: the stored value
|
|
2210
|
+
# with no type check, plus +value_type+. Never raises; all
|
|
2211
|
+
# exceptions become ERROR details.
|
|
2212
|
+
def build_details(key, expected_type, context)
|
|
2191
2213
|
ensure_initialized_after_fork
|
|
2192
2214
|
jit = context == NO_DEFAULT_PROVIDED ? nil : context
|
|
2193
2215
|
ctx = build_context(jit)
|
|
@@ -2234,7 +2256,7 @@ module Quonfig
|
|
|
2234
2256
|
config_type = result.config&.dig('type') || result.config&.dig(:type)
|
|
2235
2257
|
|
|
2236
2258
|
begin
|
|
2237
|
-
coerced = typed_value(key, result, expected_type)
|
|
2259
|
+
coerced = expected_type.nil? ? stored_value(result) : typed_value(key, result, expected_type)
|
|
2238
2260
|
rescue Quonfig::Errors::TypeMismatchError, Quonfig::Errors::EnvVarParseError => e
|
|
2239
2261
|
# A wrong-type value and a malformed one (the getter cannot produce
|
|
2240
2262
|
# a value of its type) are both TYPE_MISMATCH, as in sdk-go.
|
|
@@ -2257,7 +2279,8 @@ module Quonfig
|
|
|
2257
2279
|
value: coerced,
|
|
2258
2280
|
reason: reason,
|
|
2259
2281
|
variant: build_variant(reason, result.rule_index, result.weighted_value_index),
|
|
2260
|
-
flag_metadata: flag_metadata
|
|
2282
|
+
flag_metadata: flag_metadata,
|
|
2283
|
+
value_type: expected_type.nil? ? stored_value_type(result) : nil
|
|
2261
2284
|
)
|
|
2262
2285
|
rescue StandardError => e
|
|
2263
2286
|
Quonfig::EvaluationDetails.new(
|
|
@@ -2270,6 +2293,22 @@ module Quonfig
|
|
|
2270
2293
|
)
|
|
2271
2294
|
end
|
|
2272
2295
|
|
|
2296
|
+
# +result+'s resolved value in stored form (after ENV_VAR provision and
|
|
2297
|
+
# decryption) with no coercion or parse validation: a duration is its
|
|
2298
|
+
# ISO 8601 text as stored. Hashes and Arrays are copied so a caller
|
|
2299
|
+
# mutating the result cannot rewrite the cached config.
|
|
2300
|
+
def stored_value(result)
|
|
2301
|
+
copy_containers(result.raw_value)
|
|
2302
|
+
end
|
|
2303
|
+
|
|
2304
|
+
def copy_containers(value)
|
|
2305
|
+
case value
|
|
2306
|
+
when Hash then value.to_h { |k, v| [k, copy_containers(v)] }
|
|
2307
|
+
when Array then value.map { |v| copy_containers(v) }
|
|
2308
|
+
else value
|
|
2309
|
+
end
|
|
2310
|
+
end
|
|
2311
|
+
|
|
2273
2312
|
# Build the variant string per the cross-SDK spec
|
|
2274
2313
|
# (project/plans/openfeature-resolution-details.md §2).
|
|
2275
2314
|
def build_variant(reason, rule_index, weighted_value_index)
|
|
@@ -19,6 +19,10 @@ module Quonfig
|
|
|
19
19
|
# "PROVIDER_NOT_READY" — the client has no config yet (init continued
|
|
20
20
|
# on code defaults, or a caught init failure)
|
|
21
21
|
# "GENERAL" — any other failure
|
|
22
|
+
#
|
|
23
|
+
# +value_type+ is set only by Client#evaluate_details: the stored value
|
|
24
|
+
# type ("string", "int", "double", "bool", "string_list", "duration",
|
|
25
|
+
# "json", ...), or nil when nothing resolved. The typed getters leave it nil.
|
|
22
26
|
class EvaluationDetails
|
|
23
27
|
REASON_STATIC = 'STATIC'
|
|
24
28
|
REASON_TARGETING_MATCH = 'TARGETING_MATCH'
|
|
@@ -31,16 +35,17 @@ module Quonfig
|
|
|
31
35
|
ERROR_GENERAL = 'GENERAL'
|
|
32
36
|
ERROR_PROVIDER_NOT_READY = 'PROVIDER_NOT_READY'
|
|
33
37
|
|
|
34
|
-
attr_reader :value, :reason, :error_code, :error_message, :variant, :flag_metadata
|
|
38
|
+
attr_reader :value, :reason, :error_code, :error_message, :variant, :flag_metadata, :value_type
|
|
35
39
|
|
|
36
40
|
def initialize(value:, reason:, error_code: nil, error_message: nil,
|
|
37
|
-
variant: nil, flag_metadata: nil)
|
|
41
|
+
variant: nil, flag_metadata: nil, value_type: nil)
|
|
38
42
|
@value = value
|
|
39
43
|
@reason = reason
|
|
40
44
|
@error_code = error_code
|
|
41
45
|
@error_message = error_message
|
|
42
46
|
@variant = variant
|
|
43
47
|
@flag_metadata = flag_metadata
|
|
48
|
+
@value_type = value_type
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def ==(other)
|
|
@@ -50,12 +55,13 @@ module Quonfig
|
|
|
50
55
|
other.error_code == @error_code &&
|
|
51
56
|
other.error_message == @error_message &&
|
|
52
57
|
other.variant == @variant &&
|
|
53
|
-
other.flag_metadata == @flag_metadata
|
|
58
|
+
other.flag_metadata == @flag_metadata &&
|
|
59
|
+
other.value_type == @value_type
|
|
54
60
|
end
|
|
55
61
|
alias eql? ==
|
|
56
62
|
|
|
57
63
|
def hash
|
|
58
|
-
[@value, @reason, @error_code, @error_message, @variant, @flag_metadata].hash
|
|
64
|
+
[@value, @reason, @error_code, @error_message, @variant, @flag_metadata, @value_type].hash
|
|
59
65
|
end
|
|
60
66
|
|
|
61
67
|
def inspect
|
|
@@ -64,6 +70,7 @@ module Quonfig
|
|
|
64
70
|
parts << "error_message=#{@error_message.inspect}" if @error_message
|
|
65
71
|
parts << "variant=#{@variant.inspect}" if @variant
|
|
66
72
|
parts << "flag_metadata=#{@flag_metadata.inspect}" if @flag_metadata
|
|
73
|
+
parts << "value_type=#{@value_type.inspect}" if @value_type
|
|
67
74
|
"#<Quonfig::EvaluationDetails #{parts.join(' ')}>"
|
|
68
75
|
end
|
|
69
76
|
end
|
data/lib/quonfig/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quonfig
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeff Dwyer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|