quonfig-openfeature 1.0.3 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfe40285fc303f81102212e0c9f7e3df54ec3c1b502df6fddab51f6b69c8b5ff
4
- data.tar.gz: c76f4b36468e11354f11a133b64cfd5766ae0b3a31591d30aa70ce100b62cfd4
3
+ metadata.gz: 20da9ec1d24f20fa576a6430d8cd765b6d821a2cc052e0bfb8f59a5511b8447a
4
+ data.tar.gz: 333ea7e9395c479af7e7768bdb256c79860e7c495e009e9c51e8d14af51807e4
5
5
  SHA512:
6
- metadata.gz: bf6a9629d4a67816b6e6c42da14a20b13194d63a6fda798e81249fe2029872537b5edd245cf6587f377244b23963bd5ba8fce8a6e3bcf20e142a212311ed2aee
7
- data.tar.gz: a3834afebc880a6f26fe4c3f40e8cf6253393a0a1c47c996df06bef2b7c8df50e24c2c882d86bc93e86394f5b964b79274e7f3b47821d4ab55355ef49370979f
6
+ metadata.gz: b3319e1dbd57f6bcfa125befb424e8c2f811dc3c01f921b94c3426a0e62eed5f3ab100a49c372236e68adaa1fde3ca5d259d55af09800c40e0a4043393915c2d
7
+ data.tar.gz: 5cc0aecc4fcd057d426ae31381572bb27e97160dea9e154cc84e85bf668849618d5a2def62c950f3713353aef3c6f1d0c8e386055bcc35cd58b0affd1e3e0321
data/CHANGELOG.md CHANGED
@@ -1,5 +1,81 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0 - 2026-10-27
4
+
5
+ **BREAKING / UPGRADE (major, qfg-zvyy.6).** This release tracks sdk-ruby 2.0.0, the cross-SDK
6
+ typed-getter contract. Require `quonfig` (sdk-ruby) `>= 2.0.0` (floor raised from `>= 1.11.0`). No
7
+ change to this provider's code: it passes through the SDK's `*_details` results, so provider users
8
+ inherit the contract below.
9
+
10
+ ### UPGRADE to 2.0
11
+
12
+ A value a `fetch_*` method cannot read as its type returns your default with reason `ERROR` and
13
+ error code `TYPE_MISMATCH`. Strings are never coerced, and the only number conversions are exact
14
+ ones. What changes for provider users:
15
+
16
+ - `fetch_float_value` on an int config now returns the value (`30` -> `30.0`). 1.x returned your
17
+ default with `TYPE_MISMATCH`.
18
+ - `fetch_integer_value` on a double config returns the value when it is whole and within
19
+ +/-(2^53-1) (`3.0` -> `3`). 1.x returned your default for every double. `3.9`, `2^53` and `1e300`
20
+ still return your default with `TYPE_MISMATCH`, never truncated.
21
+ - `fetch_number_value` on a `duration` config returns your default with `TYPE_MISMATCH`. 1.x
22
+ returned the duration in milliseconds (`30000`, reason `STATIC`). Durations are native-only: use
23
+ `provider.client.get_duration` (integer milliseconds). `fetch_float_value` on a duration already
24
+ returned your default.
25
+ - A malformed stored int or double (for example `"twelve"`) or a malformed ENV_VAR value returns
26
+ your default with error code `TYPE_MISMATCH`, as in sdk-go. 1.x reported `GENERAL`. An unset
27
+ ENV_VAR still returns your default with `GENERAL`.
28
+ - sdk-ruby logs one WARN per key per client when a read returns your default because of a type
29
+ mismatch, a malformed value, an unset ENV_VAR or a decryption failure. The line names the key and
30
+ types, never the value. A wrong decryption key now degrades to your defaults with these WARNs.
31
+ - If you call the native client via `provider.client`: typed getters with a `default:` return the
32
+ default instead of raising, `enabled?` is true only for the bool `true` and never raises, and
33
+ `get_bool` on a stored string `"true"` is a type mismatch. See the sdk-ruby CHANGELOG
34
+ "UPGRADE to 2.0".
35
+
36
+ These already held under 1.11.0 through the provider, and the new tests now pin them: a stored
37
+ string `"true"` / `"false"` read as a boolean, a stored `"3"` read as a number, an int or bool read as
38
+ a string, an int read as a boolean, and a string or bool read as an object all return your default
39
+ with `TYPE_MISMATCH`.
40
+
41
+ ### Tests
42
+
43
+ - New `test/integration/test_typed_getter_contract.rb` (31 tests) drives the real provider through
44
+ the OpenFeature client against the integration-test-data `getter.contract.*` fixtures. 7 of the 31
45
+ fail on sdk-ruby 1.11.0; all pass on 2.0.0.
46
+
47
+ ## 1.1.0 - 2026-10-23
48
+
49
+ - **UPGRADE: raise the `quonfig` floor from `>= 1.10.0` to `>= 1.11.0`. Requires sdk-ruby 1.11.0:
50
+ SSE event and config body size caps, `Client#callback_errors_total`.** The provider code does not
51
+ change for 1.11.0. The new floor means every install of this provider gets those sdk-ruby changes:
52
+ an oversized SSE event (over 4 MiB) or config body (over 16 MiB) is treated as a failed attempt
53
+ instead of being read without limit, and `Dir.home` failures no longer raise from `Client.new`.
54
+ Read the 1.11.0 entry in the sdk-ruby CHANGELOG. Build and test against `quonfig` 1.11.0
55
+ (Gemfile.lock).
56
+ - **UPGRADE: raise the `quonfig` floor from `>= 1.7.2` to `>= 1.10.0` (qfg-9p4i.12).** The provider
57
+ now uses sdk-ruby 1.10.0's init-failure surface (`on_init_failure: :fail | :wait | :continue`,
58
+ `init_timeout_ms`, `Client#ready?` / `#wait_for_ready`, `Quonfig::Errors::InitializationError`).
59
+ Read the 1.10.0 entry in the sdk-ruby CHANGELOG: `Client.new` now retries a failed boot until
60
+ `init_timeout_ms` (default 10s) before it raises, instead of raising on the first failed attempt.
61
+ - **Added (minor): the provider emits OpenFeature events and its status follows `on_init_failure`
62
+ (init-failure design B.5).** Pass `on_init_failure:` and `init_timeout_ms:` to `Provider.new`; they
63
+ are forwarded to `Quonfig::Client.new` as before.
64
+ - `:fail` (the default): `init` raises `Quonfig::Errors::InitializationError` when no config loads,
65
+ so `set_provider_and_wait` raises and the provider is ERROR. sdk-ruby returns no client under
66
+ `:fail`, so nothing retries in the background; the process restart is the retry.
67
+ - `:wait`: `init` blocks until config loads. The provider is NOT_READY for the whole wait, then READY.
68
+ - `:continue` with no config: `init` resolves, then the provider emits `PROVIDER_ERROR` (error code
69
+ `PROVIDER_NOT_READY`), so its status is ERROR rather than READY while every evaluation returns the
70
+ code default. When the client's background retry loads config, the provider emits
71
+ `PROVIDER_READY`. Before, the provider reported READY and emitted no events.
72
+ - A client injected with `client:` gets the same treatment: if it has no config at `init`, the
73
+ provider reports ERROR until it heals.
74
+ - **Fix: an evaluation before config loads reports `PROVIDER_NOT_READY`.** sdk-ruby 1.10.0's
75
+ `*_details` getters return error code `PROVIDER_NOT_READY` on a client with no config; the provider
76
+ mapped that to `GENERAL`. `Quonfig::Errors::InitializationError` (any reason, not only the timeout
77
+ subclass) now maps to `PROVIDER_NOT_READY` too.
78
+
3
79
  ## 1.0.3 - 2026-10-16
4
80
 
5
81
  - Build and test against `quonfig` 1.8.0 (Gemfile.lock). The runtime floor stays at `>= 1.7.2`: this
data/README.md CHANGED
@@ -56,6 +56,26 @@ is_pro = client.fetch_boolean_value(flag_key: 'pro-feature', default_value: fals
56
56
  evaluation_context: ctx)
57
57
  ```
58
58
 
59
+ ## Startup failure and provider status
60
+
61
+ The provider forwards `on_init_failure:` and `init_timeout_ms:` to `Quonfig::Client.new`, and its
62
+ OpenFeature status follows the mode:
63
+
64
+ | `on_init_failure` | `init` when no config loads | Provider status | Later |
65
+ |-------------------|-----------------------------|-----------------|-------|
66
+ | `:fail` (default) | raises `Quonfig::Errors::InitializationError` after `init_timeout_ms` (at once if the SDK key is rejected) | ERROR | No client exists, so nothing retries; restart the process |
67
+ | `:wait` | blocks and retries with no upper bound | NOT_READY while waiting | READY when config loads |
68
+ | `:continue` | returns; emits `PROVIDER_ERROR` | ERROR; evaluations return your default with `PROVIDER_NOT_READY` | emits `PROVIDER_READY` when the background retry loads config |
69
+
70
+ ```ruby
71
+ provider = Quonfig::OpenFeature::Provider.new(
72
+ sdk_key: 'qf_sk_production_...',
73
+ on_init_failure: :continue, # :fail (default), :wait or :continue
74
+ init_timeout_ms: 10_000
75
+ )
76
+ OpenFeature::SDK.set_provider_and_wait(provider)
77
+ ```
78
+
59
79
  ## Context mapping
60
80
 
61
81
  OpenFeature context is flat; Quonfig context is nested by namespace. This provider
@@ -114,13 +134,30 @@ Quonfig features require the native `quonfig` SDK:
114
134
 
115
135
  1. **Log levels** -- `should_log?` and the `semantic_logger_filter` are native-only.
116
136
  2. **`string_list` configs** -- accessed via `fetch_object_value` and used as `Array<String>`.
117
- 3. **`duration` configs** -- exposed only via the native `get_duration` (returns milliseconds).
137
+ 3. **`duration` configs** -- native-only: read them with `provider.client.get_duration` (integer
138
+ milliseconds). `fetch_float_value` and `fetch_number_value` on a duration config return your
139
+ default with `TYPE_MISMATCH`.
118
140
  4. **`bytes` configs** -- not accessible via OpenFeature (no binary type in OF).
119
141
  5. **`keys` / raw config inspection** -- native-only via `provider.client`.
120
142
  6. **Context keys use dot-notation** -- `"user.email"`, not nested hashes.
121
143
  7. **`targeting_key` maps to `user.id` by default** -- configure
122
144
  `targeting_key_mapping` if you target a different namespace.
123
145
 
146
+ ## Typed reads
147
+
148
+ Each `fetch_*` method reads only values of its type (the cross-SDK typed-getter contract,
149
+ `quonfig` >= 2.0.0). A value of another type returns your default with reason `ERROR` and error
150
+ code `TYPE_MISMATCH`:
151
+
152
+ - A stored string is never coerced: `"false"` is not a boolean and `"3"` is not a number.
153
+ - Numbers convert only when exact: `fetch_float_value` on an int config returns `30.0`, and
154
+ `fetch_integer_value` on a double config returns the value only when it is whole and within
155
+ +/-(2^53-1) (`3.0` -> `3`; `3.9` is a mismatch, never truncated).
156
+ - A malformed stored or ENV_VAR value (an int `"twelve"`) also returns your default with
157
+ `TYPE_MISMATCH`. An unset ENV_VAR returns your default with `GENERAL`.
158
+
159
+ sdk-ruby logs one WARN per key when a read returns your default for one of these reasons.
160
+
124
161
  ## Offline / test mode
125
162
 
126
163
  Pass `datadir:` to evaluate against a Quonfig workspace on disk -- the same
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.3
1
+ 2.0.0
@@ -21,7 +21,7 @@ module Quonfig
21
21
  return ErrorCode::TYPE_MISMATCH if defined?(::Quonfig::Errors::TypeMismatchError) && err.is_a?(::Quonfig::Errors::TypeMismatchError)
22
22
 
23
23
  if (defined?(::Quonfig::Errors::UninitializedError) && err.is_a?(::Quonfig::Errors::UninitializedError)) ||
24
- (defined?(::Quonfig::Errors::InitializationTimeoutError) && err.is_a?(::Quonfig::Errors::InitializationTimeoutError))
24
+ (defined?(::Quonfig::Errors::InitializationError) && err.is_a?(::Quonfig::Errors::InitializationError))
25
25
  return ErrorCode::PROVIDER_NOT_READY
26
26
  end
27
27
 
@@ -31,9 +31,26 @@ module Quonfig
31
31
  #
32
32
  # client = OpenFeature::SDK.build_client
33
33
  # client.fetch_boolean_value(flag_key: 'my-flag', default_value: false)
34
+ #
35
+ # Provider status follows the SDK's +on_init_failure+ mode (init-failure
36
+ # design B.5). Pass +on_init_failure:+ and +init_timeout_ms:+ through
37
+ # +Provider.new+; they are forwarded to +Quonfig::Client.new+.
38
+ #
39
+ # * +:fail+ (default): +init+ raises, so the provider is ERROR. sdk-ruby
40
+ # returns no client, so nothing retries; restart the process.
41
+ # * +:wait+: +init+ blocks until config loads (NOT_READY), then READY.
42
+ # * +:continue+: +init+ resolves; with no config yet the provider emits
43
+ # PROVIDER_ERROR (ERROR, evaluations return the default with
44
+ # PROVIDER_NOT_READY), then PROVIDER_READY when the client heals.
34
45
  class Provider
46
+ include ::OpenFeature::SDK::Provider::EventEmitter
47
+
35
48
  NAME = 'quonfig'
36
49
 
50
+ # How long the heal watcher waits for OpenFeature to record the READY it
51
+ # dispatches when +init+ returns, before it reports PROVIDER_ERROR.
52
+ INIT_DISPATCH_WAIT_S = 5
53
+
37
54
  ResolutionDetails = ::OpenFeature::SDK::Provider::ResolutionDetails
38
55
  Reason = ::OpenFeature::SDK::Provider::Reason
39
56
  ErrorCode = ::OpenFeature::SDK::Provider::ErrorCode
@@ -63,16 +80,19 @@ module Quonfig
63
80
  environment: environment,
64
81
  extra: quonfig_options
65
82
  )
66
- @initialized = !@client.nil?
83
+ @initialized = false
67
84
  end
68
85
 
69
86
  # Initialize the underlying Quonfig client. Called by
70
- # +OpenFeature::SDK.set_provider_and_wait+.
87
+ # +OpenFeature::SDK.set_provider_and_wait+. Raises under
88
+ # +on_init_failure: :fail+ when no config loads, which OpenFeature
89
+ # records as ERROR.
71
90
  def init(_evaluation_context = nil)
72
91
  return if @initialized
73
92
 
74
- @client = ::Quonfig::Client.new(@quonfig_options)
93
+ @client ||= ::Quonfig::Client.new(@quonfig_options)
75
94
  @initialized = true
95
+ watch_for_heal(@client) unless client_ready?(@client)
76
96
  nil
77
97
  end
78
98
 
@@ -83,6 +103,8 @@ module Quonfig
83
103
  @client = nil
84
104
  @initialized = false
85
105
  client&.stop
106
+ @heal_thread&.join(1)
107
+ @heal_thread = nil
86
108
  nil
87
109
  end
88
110
 
@@ -141,6 +163,41 @@ module Quonfig
141
163
 
142
164
  private
143
165
 
166
+ def client_ready?(client)
167
+ !client.respond_to?(:ready?) || client.ready?
168
+ end
169
+
170
+ # +on_init_failure: :continue+ left the client without config. Report
171
+ # ERROR once OpenFeature has recorded the READY it dispatches after +init+
172
+ # returns, then READY when the client's background retry heals.
173
+ def watch_for_heal(client)
174
+ @heal_thread = Thread.new do
175
+ await_init_dispatch
176
+ unless client.ready?
177
+ emit_event(::OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR,
178
+ error_code: ErrorCode::PROVIDER_NOT_READY,
179
+ message: 'Quonfig has no config yet (on_init_failure: :continue); ' \
180
+ 'serving code defaults while it retries')
181
+ end
182
+ emit_event(::OpenFeature::SDK::ProviderEvent::PROVIDER_READY) if client.wait_for_ready && @client.equal?(client)
183
+ end
184
+ end
185
+
186
+ # OpenFeature dispatches PROVIDER_READY after +init+ returns. Wait for it
187
+ # so our PROVIDER_ERROR lands after it, not before. +provider_state+ is
188
+ # private in openfeature-sdk 0.5.x and public in 0.6.x.
189
+ def await_init_dispatch
190
+ config = @configuration
191
+ return unless config.respond_to?(:provider_state, true)
192
+
193
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + INIT_DISPATCH_WAIT_S
194
+ while config.send(:provider_state, self) == ::OpenFeature::SDK::ProviderState::NOT_READY
195
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
196
+
197
+ sleep 0.01
198
+ end
199
+ end
200
+
144
201
  # Map a Quonfig::EvaluationDetails to the OpenFeature ResolutionDetails
145
202
  # the SDK consumes. The Quonfig SDK's *_details methods don't raise, so
146
203
  # this is a pure mapping layer.
@@ -187,6 +244,7 @@ module Quonfig
187
244
  case error_code
188
245
  when ::Quonfig::EvaluationDetails::ERROR_FLAG_NOT_FOUND then ErrorCode::FLAG_NOT_FOUND
189
246
  when ::Quonfig::EvaluationDetails::ERROR_TYPE_MISMATCH then ErrorCode::TYPE_MISMATCH
247
+ when ::Quonfig::EvaluationDetails::ERROR_PROVIDER_NOT_READY then ErrorCode::PROVIDER_NOT_READY
190
248
  else ErrorCode::GENERAL
191
249
  end
192
250
  end
@@ -204,7 +262,7 @@ module Quonfig
204
262
 
205
263
  mapped_ctx = Context.map_context(evaluation_context, @targeting_key_mapping)
206
264
  yield(client, mapped_ctx)
207
- rescue ::Quonfig::Errors::UninitializedError, ::Quonfig::Errors::InitializationTimeoutError => e
265
+ rescue ::Quonfig::Errors::UninitializedError, ::Quonfig::Errors::InitializationError => e
208
266
  ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
209
267
  error_code: ErrorCode::PROVIDER_NOT_READY,
210
268
  error_message: e.message)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quonfig-openfeature
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 2.0.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-16 00:00:00.000000000 Z
11
+ date: 2026-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: openfeature-sdk
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 1.7.2
33
+ version: 2.0.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 1.7.2
40
+ version: 2.0.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement