quonfig 0.0.12 → 0.0.14
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 +9 -0
- data/lib/quonfig/client.rb +52 -5
- data/lib/quonfig/evaluation_details.rb +11 -4
- data/lib/quonfig/evaluator.rb +13 -0
- 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: b25ea20d7f44acff4ed82e17522a9fb6055791c4f1e0c861075974e5ae37421f
|
|
4
|
+
data.tar.gz: e0c260d2d13926e21f2525c7686a24f8dec2f1fa998efa039db59baf4447cd60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da91dbd4f9cc300f2dab9e8f39a73033e642d94272288cbcacf4358eb28f4f9b064f8fbe8301c5c26e1b342cd3cd76179d362029e06379bcac39685c3a050cb2
|
|
7
|
+
data.tar.gz: ac77088e6a6e0256d947f40b26abb9527bb55cff8a3fa39eaaebf91c43746379d5fa2325bd06e049922b2cbc8521f78252bdd79106c6f1ae7f1a0264f4033ab6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.14 - 2026-05-10
|
|
4
|
+
|
|
5
|
+
- **Feat: expose `variant` and `flag_metadata` on `EvaluationDetails` (qfg-9dbl).** OpenFeature's `EvaluationDetails` Ruby return type now carries the variant name and the flag-level metadata hash alongside the resolved value/reason. Brings sdk-ruby to parity with the other SDKs' detail surfaces and lets host apps (incl. the Ruby OpenFeature provider) read variant/metadata without re-fetching the config.
|
|
6
|
+
- **Test: regenerate integration tests from rubocop-clean templates (qfg-vrck).** The integration suite under `test/integration/` is now generated from templates that pass `bundle exec rubocop` on first emit, so future regenerations don't trigger a follow-up autofix commit.
|
|
7
|
+
|
|
8
|
+
## 0.0.13 - 2026-05-07
|
|
9
|
+
|
|
10
|
+
- **Feat: `IS_PRESENT` and `IS_NOT_PRESENT` targeting operators (qfg-7jnb.6).** Both take only `propertyName` (no `valueToMatch`). `IS_PRESENT` resolves the dotted path against the merged context and returns true iff the value is non-nil. Type-agnostic — empty string `""`, `0`, and `false` all count as **present**; only `nil` / missing keys (including missing nested paths) are absent. `IS_NOT_PRESENT` is the negation. Implemented explicitly without ActiveSupport's `present?` / `blank?`, which would have given the wrong semantics on `""` and `false`. Matches sdk-node, sdk-go, sdk-python, sdk-ruby, sdk-javascript wire behaviour. Closes the integration-test parity gap that left 7 RSpec/Minitest cases red since the operators landed in `integration-test-data`.
|
|
11
|
+
|
|
3
12
|
## 0.0.12 - 2026-05-03
|
|
4
13
|
|
|
5
14
|
- **Feat: pluggable `logger:` kwarg on `Quonfig::Client.new`.** Host apps can now pass `Rails.logger` (or any stdlib `Logger`-compatible instance) and have all SDK warnings/errors flow through it instead of bare stderr / SemanticLogger. Implemented as a class-level `Quonfig::InternalLogger.user_logger` override that all `LOG` constants respect at log-call time, so existing per-class `LOG` constants pick it up automatically. Duck-typed (responds to `debug`/`info`/`warn`/`error`); missing levels degrade gracefully. SemanticLogger auto-detection is unchanged when no logger is supplied. Also routes the two outlier `dev_context.rb` warns (file read / JSON parse failures) through `InternalLogger` so they pick up the host-supplied logger too. (qfg-mol-1qw.3)
|
data/lib/quonfig/client.rb
CHANGED
|
@@ -547,19 +547,25 @@ module Quonfig
|
|
|
547
547
|
value: nil,
|
|
548
548
|
reason: Quonfig::EvaluationDetails::REASON_ERROR,
|
|
549
549
|
error_code: Quonfig::EvaluationDetails::ERROR_FLAG_NOT_FOUND,
|
|
550
|
-
error_message: e.message
|
|
550
|
+
error_message: e.message,
|
|
551
|
+
variant: build_variant(Quonfig::EvaluationDetails::REASON_ERROR, nil, nil),
|
|
552
|
+
flag_metadata: build_flag_metadata(nil, nil, nil, nil, nil)
|
|
551
553
|
)
|
|
552
554
|
end
|
|
553
555
|
|
|
554
556
|
if result.nil?
|
|
555
557
|
return Quonfig::EvaluationDetails.new(
|
|
556
558
|
value: nil,
|
|
557
|
-
reason: Quonfig::EvaluationDetails::REASON_DEFAULT
|
|
559
|
+
reason: Quonfig::EvaluationDetails::REASON_DEFAULT,
|
|
560
|
+
variant: build_variant(Quonfig::EvaluationDetails::REASON_DEFAULT, nil, nil),
|
|
561
|
+
flag_metadata: build_flag_metadata(nil, nil, nil, nil, nil)
|
|
558
562
|
)
|
|
559
563
|
end
|
|
560
564
|
|
|
561
565
|
record_evaluation_for_telemetry(result)
|
|
562
566
|
|
|
567
|
+
config_id = result.config&.dig('id') || result.config&.dig(:id)
|
|
568
|
+
config_type = result.config&.dig('type') || result.config&.dig(:type)
|
|
563
569
|
raw_value = result.unwrapped_value
|
|
564
570
|
|
|
565
571
|
begin
|
|
@@ -569,23 +575,64 @@ module Quonfig
|
|
|
569
575
|
value: nil,
|
|
570
576
|
reason: Quonfig::EvaluationDetails::REASON_ERROR,
|
|
571
577
|
error_code: Quonfig::EvaluationDetails::ERROR_TYPE_MISMATCH,
|
|
572
|
-
error_message: e.message
|
|
578
|
+
error_message: e.message,
|
|
579
|
+
variant: build_variant(Quonfig::EvaluationDetails::REASON_ERROR, nil, nil),
|
|
580
|
+
flag_metadata: build_flag_metadata(config_id, config_type, nil, nil, nil)
|
|
573
581
|
)
|
|
574
582
|
end
|
|
575
583
|
|
|
584
|
+
reason = result.of_reason
|
|
576
585
|
Quonfig::EvaluationDetails.new(
|
|
577
586
|
value: coerced,
|
|
578
|
-
reason:
|
|
587
|
+
reason: reason,
|
|
588
|
+
variant: build_variant(reason, result.rule_index, result.weighted_value_index),
|
|
589
|
+
flag_metadata: build_flag_metadata(
|
|
590
|
+
config_id, config_type, result.rule_index, result.weighted_value_index, reason
|
|
591
|
+
)
|
|
579
592
|
)
|
|
580
593
|
rescue StandardError => e
|
|
581
594
|
Quonfig::EvaluationDetails.new(
|
|
582
595
|
value: nil,
|
|
583
596
|
reason: Quonfig::EvaluationDetails::REASON_ERROR,
|
|
584
597
|
error_code: Quonfig::EvaluationDetails::ERROR_GENERAL,
|
|
585
|
-
error_message: e.message
|
|
598
|
+
error_message: e.message,
|
|
599
|
+
variant: build_variant(Quonfig::EvaluationDetails::REASON_ERROR, nil, nil),
|
|
600
|
+
flag_metadata: build_flag_metadata(nil, nil, nil, nil, nil)
|
|
586
601
|
)
|
|
587
602
|
end
|
|
588
603
|
|
|
604
|
+
# Build the variant string per the cross-SDK spec
|
|
605
|
+
# (project/plans/openfeature-resolution-details.md §2).
|
|
606
|
+
def build_variant(reason, rule_index, weighted_value_index)
|
|
607
|
+
case reason
|
|
608
|
+
when Quonfig::EvaluationDetails::REASON_STATIC
|
|
609
|
+
'static'
|
|
610
|
+
when Quonfig::EvaluationDetails::REASON_TARGETING_MATCH
|
|
611
|
+
"targeting:#{rule_index || 0}"
|
|
612
|
+
when Quonfig::EvaluationDetails::REASON_SPLIT
|
|
613
|
+
"split:#{weighted_value_index || 0}"
|
|
614
|
+
else
|
|
615
|
+
'default'
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Build the flag_metadata hash per the cross-SDK spec
|
|
620
|
+
# (project/plans/openfeature-resolution-details.md §3) using Ruby's
|
|
621
|
+
# snake_case keys and the wire's snake_case config_type values.
|
|
622
|
+
def build_flag_metadata(config_id, config_type, rule_index, weighted_value_index, reason)
|
|
623
|
+
md = {}
|
|
624
|
+
md['config_id'] = config_id if config_id
|
|
625
|
+
md['config_type'] = config_type if config_type
|
|
626
|
+
env = @options.environment
|
|
627
|
+
md['environment'] = env if env && !env.empty?
|
|
628
|
+
if rule_index && rule_index >= 0 &&
|
|
629
|
+
[Quonfig::EvaluationDetails::REASON_TARGETING_MATCH, Quonfig::EvaluationDetails::REASON_SPLIT].include?(reason)
|
|
630
|
+
md['rule_index'] = rule_index
|
|
631
|
+
end
|
|
632
|
+
md['weighted_value_index'] = weighted_value_index if weighted_value_index && reason == Quonfig::EvaluationDetails::REASON_SPLIT
|
|
633
|
+
md
|
|
634
|
+
end
|
|
635
|
+
|
|
589
636
|
def typed_get(key, expected_type, default:, context:)
|
|
590
637
|
jit = context == NO_DEFAULT_PROVIDED ? NO_DEFAULT_PROVIDED : context
|
|
591
638
|
value = get(key, default, jit)
|
|
@@ -28,13 +28,16 @@ module Quonfig
|
|
|
28
28
|
ERROR_TYPE_MISMATCH = 'TYPE_MISMATCH'
|
|
29
29
|
ERROR_GENERAL = 'GENERAL'
|
|
30
30
|
|
|
31
|
-
attr_reader :value, :reason, :error_code, :error_message
|
|
31
|
+
attr_reader :value, :reason, :error_code, :error_message, :variant, :flag_metadata
|
|
32
32
|
|
|
33
|
-
def initialize(value:, reason:, error_code: nil, error_message: nil
|
|
33
|
+
def initialize(value:, reason:, error_code: nil, error_message: nil,
|
|
34
|
+
variant: nil, flag_metadata: nil)
|
|
34
35
|
@value = value
|
|
35
36
|
@reason = reason
|
|
36
37
|
@error_code = error_code
|
|
37
38
|
@error_message = error_message
|
|
39
|
+
@variant = variant
|
|
40
|
+
@flag_metadata = flag_metadata
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def ==(other)
|
|
@@ -42,18 +45,22 @@ module Quonfig
|
|
|
42
45
|
other.value == @value &&
|
|
43
46
|
other.reason == @reason &&
|
|
44
47
|
other.error_code == @error_code &&
|
|
45
|
-
other.error_message == @error_message
|
|
48
|
+
other.error_message == @error_message &&
|
|
49
|
+
other.variant == @variant &&
|
|
50
|
+
other.flag_metadata == @flag_metadata
|
|
46
51
|
end
|
|
47
52
|
alias eql? ==
|
|
48
53
|
|
|
49
54
|
def hash
|
|
50
|
-
[@value, @reason, @error_code, @error_message].hash
|
|
55
|
+
[@value, @reason, @error_code, @error_message, @variant, @flag_metadata].hash
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
def inspect
|
|
54
59
|
parts = ["value=#{@value.inspect}", "reason=#{@reason.inspect}"]
|
|
55
60
|
parts << "error_code=#{@error_code.inspect}" if @error_code
|
|
56
61
|
parts << "error_message=#{@error_message.inspect}" if @error_message
|
|
62
|
+
parts << "variant=#{@variant.inspect}" if @variant
|
|
63
|
+
parts << "flag_metadata=#{@flag_metadata.inspect}" if @flag_metadata
|
|
57
64
|
"#<Quonfig::EvaluationDetails #{parts.join(' ')}>"
|
|
58
65
|
end
|
|
59
66
|
end
|
data/lib/quonfig/evaluator.rb
CHANGED
|
@@ -51,6 +51,8 @@ module Quonfig
|
|
|
51
51
|
OP_PROP_SEMVER_GREATER_THAN = 'PROP_SEMVER_GREATER_THAN'
|
|
52
52
|
OP_IN_SEG = 'IN_SEG'
|
|
53
53
|
OP_NOT_IN_SEG = 'NOT_IN_SEG'
|
|
54
|
+
OP_IS_PRESENT = 'IS_PRESENT'
|
|
55
|
+
OP_IS_NOT_PRESENT = 'IS_NOT_PRESENT'
|
|
54
56
|
|
|
55
57
|
MAGIC_CURRENT_TIME_PROPS = %w[quonfig.current-time prefab.current-time reforge.current-time].freeze
|
|
56
58
|
|
|
@@ -280,6 +282,17 @@ module Quonfig
|
|
|
280
282
|
end
|
|
281
283
|
false
|
|
282
284
|
|
|
285
|
+
when OP_IS_PRESENT, OP_IS_NOT_PRESENT
|
|
286
|
+
# Presence is type-agnostic: empty string "", 0, and false are all
|
|
287
|
+
# PRESENT. Only nil / missing key (incl. nested) is NOT present.
|
|
288
|
+
# `lookup_context` already returns context_exists=true iff the
|
|
289
|
+
# resolved value is non-nil — exactly the semantic we need. Do not
|
|
290
|
+
# use ActiveSupport's #present? / #blank? here: they treat "" and
|
|
291
|
+
# false as blank, which is the wrong behaviour for this operator.
|
|
292
|
+
return context_exists if operator == OP_IS_PRESENT
|
|
293
|
+
|
|
294
|
+
!context_exists
|
|
295
|
+
|
|
283
296
|
when OP_IN_SEG, OP_NOT_IN_SEG
|
|
284
297
|
if match_value
|
|
285
298
|
segment_key = to_s_nil(hget(match_value, :value))
|
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: 0.0.
|
|
4
|
+
version: 0.0.14
|
|
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-05-
|
|
11
|
+
date: 2026-05-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|