quonfig-openfeature 2.0.0 → 2.2.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: 20da9ec1d24f20fa576a6430d8cd765b6d821a2cc052e0bfb8f59a5511b8447a
4
- data.tar.gz: 333ea7e9395c479af7e7768bdb256c79860e7c495e009e9c51e8d14af51807e4
3
+ metadata.gz: d4a4af7330ac53ebeb637ed61d4c7cd0530bcfe62db92b845e5233d105496a3b
4
+ data.tar.gz: 7761829a3596aace1dad718f622b1156656079fd9ffe15123fcf0114c06e9837
5
5
  SHA512:
6
- metadata.gz: b3319e1dbd57f6bcfa125befb424e8c2f811dc3c01f921b94c3426a0e62eed5f3ab100a49c372236e68adaa1fde3ca5d259d55af09800c40e0a4043393915c2d
7
- data.tar.gz: 5cc0aecc4fcd057d426ae31381572bb27e97160dea9e154cc84e85bf668849618d5a2def62c950f3713353aef3c6f1d0c8e386055bcc35cd58b0affd1e3e0321
6
+ metadata.gz: 1de71990148d10403dcc3559f5a2d07d16a73a3c6c920e1fc38bdd282a8ba2fbd2d6e9e10524c1b8f04195c337918757f51d94365ec8368824aaccc52da17e8e
7
+ data.tar.gz: 80802c8ddb10fada30d464bbbaf55a9b45e45d05a819980dd555d601568c01003b2d8de9aa5c3f970727e39a4e0d5bcfe5c370d29b82bd11db4b33de987d5ae0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.2.0 - 2026-10-29
4
+
5
+ `fetch_string_value` on a `duration` config returns its stored ISO 8601 text again (reason
6
+ `STATIC`), as in openfeature-go and the shared OpenFeature cases (qfg-177f). The text comes back
7
+ unchanged: `"PT90S"` stays `"PT90S"`, not `"PT1M30S"`. 2.x returned your default with
8
+ `TYPE_MISMATCH`. Number reads of a duration still return your default. A malformed stored duration
9
+ such as `"30s"` still returns your default with `PARSE_ERROR`, and so does a duration ENV_VAR set
10
+ to a malformed value.
11
+
12
+ Requires quonfig (sdk-ruby) 2.1.0: the text is read with its new public `evaluate_details`, in one
13
+ evaluation, and checked against the shared duration grammar with `Quonfig::Duration.valid?`.
14
+
15
+ ## 2.1.0 - 2026-10-28
16
+
17
+ A malformed value now resolves with error code `PARSE_ERROR` instead of `TYPE_MISMATCH`
18
+ (qfg-9xsl, mirrors integration-test-data `tests/openfeature/openfeature.yaml` Case 6). You still
19
+ get your default with reason `ERROR`. This covers a value of a type the `fetch_*` method reads that
20
+ does not parse:
21
+
22
+ - a stored int `"twelve"` or double `"abc"`;
23
+ - an int or double provided by an ENV_VAR set to an unparseable string, such as `"twelve"`;
24
+ - a malformed stored duration such as `"30s"`, read with `fetch_string_value`.
25
+
26
+ A read with the method for another type (an int config read with `fetch_boolean_value`) stays
27
+ `TYPE_MISMATCH`, as does a number that parses but does not convert exactly (`fetch_integer_value`
28
+ on `3.9`). sdk-ruby's `*_details` report both cases as `TYPE_MISMATCH`, so the provider tells them
29
+ apart: on a `TYPE_MISMATCH` it checks the config's stored `valueType` and, if the method can read
30
+ that type, reads it with the native getter for that type. If that read fails too, the value is
31
+ malformed. This extra read happens only on the error path.
32
+
33
+ If you alert or branch on `TYPE_MISMATCH` for bad config data, match `PARSE_ERROR` as well.
34
+
3
35
  ## 2.0.0 - 2026-10-27
4
36
 
5
37
  **BREAKING / UPGRADE (major, qfg-zvyy.6).** This release tracks sdk-ruby 2.0.0, the cross-SDK
data/README.md CHANGED
@@ -134,9 +134,9 @@ Quonfig features require the native `quonfig` SDK:
134
134
 
135
135
  1. **Log levels** -- `should_log?` and the `semantic_logger_filter` are native-only.
136
136
  2. **`string_list` configs** -- accessed via `fetch_object_value` and used as `Array<String>`.
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`.
137
+ 3. **`duration` configs** -- `fetch_string_value` returns the stored ISO 8601 text unchanged
138
+ (`"PT90S"`). For integer milliseconds use `provider.client.get_duration`. `fetch_float_value`
139
+ and `fetch_number_value` on a duration config return your default with `TYPE_MISMATCH`.
140
140
  4. **`bytes` configs** -- not accessible via OpenFeature (no binary type in OF).
141
141
  5. **`keys` / raw config inspection** -- native-only via `provider.client`.
142
142
  6. **Context keys use dot-notation** -- `"user.email"`, not nested hashes.
@@ -153,8 +153,10 @@ code `TYPE_MISMATCH`:
153
153
  - Numbers convert only when exact: `fetch_float_value` on an int config returns `30.0`, and
154
154
  `fetch_integer_value` on a double config returns the value only when it is whole and within
155
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`.
156
+ - A malformed value of a type the method reads returns your default with `PARSE_ERROR`: a stored
157
+ int `"twelve"`, an int or double ENV_VAR set to `"twelve"`, or a stored duration `"30s"` read
158
+ with `fetch_string_value`. A read with the method for another type stays `TYPE_MISMATCH`. An
159
+ unset ENV_VAR returns your default with `GENERAL`.
158
160
 
159
161
  sdk-ruby logs one WARN per key when a read returns your default for one of these reasons.
160
162
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.0
1
+ 2.2.0
@@ -56,6 +56,32 @@ module Quonfig
56
56
  ErrorCode = ::OpenFeature::SDK::Provider::ErrorCode
57
57
  ProviderMetadata = ::OpenFeature::SDK::Provider::ProviderMetadata
58
58
 
59
+ # The stored +valueType+s each fetch method can read: its own type plus
60
+ # the ones it converts exactly (int <-> double, duration -> ISO string).
61
+ # A TYPE_MISMATCH on a config of one of these types means the value
62
+ # did not parse, so it is reported as PARSE_ERROR (qfg-9xsl).
63
+ READABLE_VALUE_TYPES = {
64
+ boolean: %w[bool],
65
+ string: %w[string log_level duration],
66
+ integer: %w[int double],
67
+ float: %w[double int],
68
+ number: %w[int double],
69
+ object: %w[string_list json]
70
+ }.freeze
71
+
72
+ # The native details getter that reads each stored +valueType+ as itself.
73
+ NATIVE_DETAILS_GETTER = {
74
+ 'bool' => :get_bool_details,
75
+ 'string' => :get_string_details,
76
+ 'log_level' => :get_string_details,
77
+ 'duration' => :get_duration_details,
78
+ 'int' => :get_int_details,
79
+ 'double' => :get_float_details,
80
+ 'string_list' => :get_string_list_details,
81
+ 'json' => :get_json_details
82
+ }.freeze
83
+ private_constant :READABLE_VALUE_TYPES, :NATIVE_DETAILS_GETTER
84
+
59
85
  attr_reader :metadata, :targeting_key_mapping
60
86
 
61
87
  # @param sdk_key [String, nil] SDK key for the live delivery service.
@@ -117,13 +143,19 @@ module Quonfig
117
143
 
118
144
  def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
119
145
  evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
120
- to_resolution(client.get_bool_details(flag_key, context: mapped_ctx), default_value)
146
+ details = client.get_bool_details(flag_key, context: mapped_ctx)
147
+ to_resolution(details, default_value, parse_error: malformed?(client, flag_key, mapped_ctx, :boolean, details))
121
148
  end
122
149
  end
123
150
 
151
+ # A +duration+ config reads as its stored ISO 8601 text, unchanged
152
+ # ("PT90S" stays "PT90S"): OpenFeature has no duration type (qfg-177f).
124
153
  def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
125
154
  evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
126
- to_resolution(client.get_string_details(flag_key, context: mapped_ctx), default_value)
155
+ next duration_iso_resolution(client, flag_key, mapped_ctx, default_value) if stored_value_type(client, flag_key) == 'duration'
156
+
157
+ details = client.get_string_details(flag_key, context: mapped_ctx)
158
+ to_resolution(details, default_value, parse_error: malformed?(client, flag_key, mapped_ctx, :string, details))
127
159
  end
128
160
  end
129
161
 
@@ -134,19 +166,21 @@ module Quonfig
134
166
  evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
135
167
  details = client.get_int_details(flag_key, context: mapped_ctx)
136
168
  details = client.get_float_details(flag_key, context: mapped_ctx) if details.error_code == ::Quonfig::EvaluationDetails::ERROR_TYPE_MISMATCH
137
- to_resolution(details, default_value)
169
+ to_resolution(details, default_value, parse_error: malformed?(client, flag_key, mapped_ctx, :number, details))
138
170
  end
139
171
  end
140
172
 
141
173
  def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
142
174
  evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
143
- to_resolution(client.get_int_details(flag_key, context: mapped_ctx), default_value)
175
+ details = client.get_int_details(flag_key, context: mapped_ctx)
176
+ to_resolution(details, default_value, parse_error: malformed?(client, flag_key, mapped_ctx, :integer, details))
144
177
  end
145
178
  end
146
179
 
147
180
  def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
148
181
  evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
149
- to_resolution(client.get_float_details(flag_key, context: mapped_ctx), default_value)
182
+ details = client.get_float_details(flag_key, context: mapped_ctx)
183
+ to_resolution(details, default_value, parse_error: malformed?(client, flag_key, mapped_ctx, :float, details))
150
184
  end
151
185
  end
152
186
 
@@ -157,7 +191,7 @@ module Quonfig
157
191
  evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
158
192
  details = client.get_string_list_details(flag_key, context: mapped_ctx)
159
193
  details = client.get_json_details(flag_key, context: mapped_ctx) if details.error_code == ::Quonfig::EvaluationDetails::ERROR_TYPE_MISMATCH
160
- to_resolution(details, default_value)
194
+ to_resolution(details, default_value, parse_error: malformed?(client, flag_key, mapped_ctx, :object, details))
161
195
  end
162
196
  end
163
197
 
@@ -198,10 +232,69 @@ module Quonfig
198
232
  end
199
233
  end
200
234
 
235
+ # True when +details+ is a TYPE_MISMATCH for a config whose value has a
236
+ # type +kind+ can read but did not parse: a stored int "twelve", an int
237
+ # ENV_VAR set to "twelve", a duration "30s" (qfg-9xsl). sdk-ruby's
238
+ # *_details report a malformed value and a wrong-type read alike as
239
+ # TYPE_MISMATCH, so this checks the stored +valueType+ and reads the
240
+ # config with the getter for that type: if that read fails too, the
241
+ # value is malformed. A config of a type +kind+ cannot read stays
242
+ # TYPE_MISMATCH, and so does a value that parsed but does not convert
243
+ # exactly (fetch_integer_value on a 3.9 double).
244
+ def malformed?(client, flag_key, mapped_ctx, kind, details)
245
+ return false unless details.error_code == ::Quonfig::EvaluationDetails::ERROR_TYPE_MISMATCH
246
+
247
+ value_type = stored_value_type(client, flag_key)
248
+ return false unless READABLE_VALUE_TYPES.fetch(kind).include?(value_type)
249
+
250
+ native = client.public_send(NATIVE_DETAILS_GETTER.fetch(value_type), flag_key, context: mapped_ctx)
251
+ native.error_code == ::Quonfig::EvaluationDetails::ERROR_TYPE_MISMATCH
252
+ end
253
+
254
+ # Read a duration config as its ISO 8601 text with one evaluation:
255
+ # sdk-ruby's evaluate_details (quonfig >= 2.1.0) returns the resolved
256
+ # value in stored form, so the text is the stored text, or an ENV_VAR's
257
+ # text for a provided duration. evaluate_details does no type check, so
258
+ # the text is validated here with Quonfig::Duration.valid?, the grammar
259
+ # get_duration uses:
260
+ #
261
+ # * valid text: the text, with the evaluation's reason, variant and
262
+ # flag_metadata.
263
+ # * malformed stored text ("30s"): the default with PARSE_ERROR.
264
+ # * malformed ENV_VAR: evaluate_details reports ERROR / TYPE_MISMATCH,
265
+ # which on a duration config can only mean a value that did not
266
+ # parse, so it is PARSE_ERROR too.
267
+ # * the structured { seconds, nanos } shape has no ISO text: the
268
+ # default with TYPE_MISMATCH.
269
+ def duration_iso_resolution(client, flag_key, mapped_ctx, default_value)
270
+ details = client.evaluate_details(flag_key, context: mapped_ctx)
271
+ if details.reason == ::Quonfig::EvaluationDetails::REASON_ERROR
272
+ parse_error = details.error_code == ::Quonfig::EvaluationDetails::ERROR_TYPE_MISMATCH
273
+ return to_resolution(details, default_value, parse_error: parse_error)
274
+ end
275
+ return to_resolution(details, default_value) if details.reason == ::Quonfig::EvaluationDetails::REASON_DEFAULT
276
+
277
+ value = details.value
278
+ return to_resolution(details, default_value) if value.is_a?(String) && ::Quonfig::Duration.valid?(value)
279
+
280
+ error_code = value.is_a?(String) ? ErrorCode::PARSE_ERROR : ErrorCode::TYPE_MISMATCH
281
+ ResolutionDetails.new(value: default_value, reason: Reason::ERROR, error_code: error_code,
282
+ error_message: "duration #{flag_key.inspect} has no valid ISO 8601 text",
283
+ variant: details.variant, flag_metadata: details.flag_metadata || {})
284
+ end
285
+
286
+ def stored_value_type(client, flag_key)
287
+ config = client.store.get(flag_key)
288
+ return nil unless config.is_a?(Hash)
289
+
290
+ (config['valueType'] || config[:valueType] || config['value_type'] || config[:value_type])&.to_s
291
+ end
292
+
201
293
  # Map a Quonfig::EvaluationDetails to the OpenFeature ResolutionDetails
202
294
  # the SDK consumes. The Quonfig SDK's *_details methods don't raise, so
203
- # this is a pure mapping layer.
204
- def to_resolution(details, default_value)
295
+ # this is a pure mapping layer. +parse_error+ reports an ERROR as
296
+ # PARSE_ERROR (see #malformed?).
297
+ def to_resolution(details, default_value, parse_error: false)
205
298
  variant = details.variant
206
299
  flag_metadata = details.flag_metadata || {}
207
300
  case details.reason
@@ -221,7 +314,7 @@ module Quonfig
221
314
  ResolutionDetails.new(
222
315
  value: default_value,
223
316
  reason: Reason::ERROR,
224
- error_code: map_error_code(details.error_code),
317
+ error_code: parse_error ? ErrorCode::PARSE_ERROR : map_error_code(details.error_code),
225
318
  error_message: details.error_message,
226
319
  variant: variant,
227
320
  flag_metadata: flag_metadata
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: 2.0.0
4
+ version: 2.2.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-27 00:00:00.000000000 Z
11
+ date: 2026-10-29 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: 2.0.0
33
+ version: 2.1.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: 2.0.0
40
+ version: 2.1.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement