quonfig-openfeature 0.0.1
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +138 -0
- data/VERSION +1 -0
- data/lib/quonfig/openfeature/context.rb +79 -0
- data/lib/quonfig/openfeature/errors.rb +48 -0
- data/lib/quonfig/openfeature/provider.rb +239 -0
- data/lib/quonfig/openfeature/version.rb +7 -0
- data/lib/quonfig/openfeature.rb +9 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 54cc3d133c6809723ef8a1e51ae646dcd889ce4792ce3cbf9acb4f1d394639d4
|
|
4
|
+
data.tar.gz: 63001d3f0a98e3ae2e7cdf7b9e5787bc6fc6da0c08a10c71a73d1d9e54724d6b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3210f73b87dd87f839f25cbbe134917ab0ccb8eea921fab1c382e3be149c538947e8dc06a8a1962b53296dff4002fe2d3fa88948b0f472302a791a368a5ef75b
|
|
7
|
+
data.tar.gz: c9195438b21b65c3ed46e27790180a4b9692d06d2dec364af8f14baa30b5bdc202b0e00668cd1200127164d8f688dc239f51a5931ee0d657484742f6039fab0c
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Quonfig
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# quonfig-openfeature
|
|
2
|
+
|
|
3
|
+
OpenFeature provider for [Quonfig](https://quonfig.com) -- Ruby server-side SDK.
|
|
4
|
+
|
|
5
|
+
This gem wraps the `quonfig` native Ruby SDK and implements the
|
|
6
|
+
[OpenFeature](https://openfeature.dev) `OpenFeature::SDK::Provider` contract.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
gem install quonfig-openfeature openfeature-sdk quonfig
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or with Bundler:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'quonfig-openfeature'
|
|
18
|
+
gem 'openfeature-sdk'
|
|
19
|
+
gem 'quonfig'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require 'quonfig/openfeature'
|
|
26
|
+
require 'open_feature/sdk'
|
|
27
|
+
|
|
28
|
+
provider = Quonfig::OpenFeature::Provider.new(
|
|
29
|
+
sdk_key: 'qf_sk_production_...'
|
|
30
|
+
# targeting_key_mapping: 'user.id', # default
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
OpenFeature::SDK.set_provider_and_wait(provider)
|
|
34
|
+
|
|
35
|
+
client = OpenFeature::SDK.build_client
|
|
36
|
+
|
|
37
|
+
# Boolean flag
|
|
38
|
+
enabled = client.fetch_boolean_value(flag_key: 'my-feature', default_value: false)
|
|
39
|
+
|
|
40
|
+
# String config
|
|
41
|
+
welcome = client.fetch_string_value(flag_key: 'welcome-message', default_value: 'Hello!')
|
|
42
|
+
|
|
43
|
+
# Number config (Integer or Float)
|
|
44
|
+
timeout = client.fetch_number_value(flag_key: 'request-timeout-ms', default_value: 5000)
|
|
45
|
+
|
|
46
|
+
# Object config (JSON or string_list)
|
|
47
|
+
allowed_plans = client.fetch_object_value(flag_key: 'allowed-plans', default_value: [])
|
|
48
|
+
|
|
49
|
+
# With evaluation context (per-request)
|
|
50
|
+
ctx = OpenFeature::SDK::EvaluationContext.new(
|
|
51
|
+
targeting_key: 'user-123', # maps to user.id by default
|
|
52
|
+
'user.plan' => 'pro',
|
|
53
|
+
'org.tier' => 'enterprise'
|
|
54
|
+
)
|
|
55
|
+
is_pro = client.fetch_boolean_value(flag_key: 'pro-feature', default_value: false,
|
|
56
|
+
evaluation_context: ctx)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Context mapping
|
|
60
|
+
|
|
61
|
+
OpenFeature context is flat; Quonfig context is nested by namespace. This provider
|
|
62
|
+
maps between them using dot-notation:
|
|
63
|
+
|
|
64
|
+
| OpenFeature context key | Quonfig namespace | Quonfig property |
|
|
65
|
+
|-------------------------|-------------------|------------------|
|
|
66
|
+
| `targeting_key` | `user` | `id` (configurable via `targeting_key_mapping`) |
|
|
67
|
+
| `"user.email"` | `user` | `email` |
|
|
68
|
+
| `"org.tier"` | `org` | `tier` |
|
|
69
|
+
| `"country"` (no dot) | `""` (default) | `country` |
|
|
70
|
+
| `"user.ip.address"` | `user` | `ip.address` (first dot only) |
|
|
71
|
+
|
|
72
|
+
### Customising `targeting_key` mapping
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
provider = Quonfig::OpenFeature::Provider.new(
|
|
76
|
+
sdk_key: 'qf_sk_...',
|
|
77
|
+
targeting_key_mapping: 'account.id' # maps targeting_key to { account: { id: ... } }
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Accessing native SDK features
|
|
82
|
+
|
|
83
|
+
The `client` reader returns the underlying `Quonfig::Client` for features not
|
|
84
|
+
available through the OpenFeature API:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
native = provider.client
|
|
88
|
+
|
|
89
|
+
# Log level integration
|
|
90
|
+
native.should_log?(logger_path: 'auth', desired_level: :debug,
|
|
91
|
+
contexts: { 'user' => { 'id' => 'user-123' } })
|
|
92
|
+
|
|
93
|
+
# List all config keys
|
|
94
|
+
keys = native.keys
|
|
95
|
+
|
|
96
|
+
# Per-request bound client
|
|
97
|
+
native.with_context('user' => { 'plan' => 'pro' }) do |bound|
|
|
98
|
+
bound.get_bool('pro-feature')
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## What you lose vs. the native SDK
|
|
103
|
+
|
|
104
|
+
OpenFeature is designed for feature flags, not general configuration. Some
|
|
105
|
+
Quonfig features require the native `quonfig` SDK:
|
|
106
|
+
|
|
107
|
+
1. **Log levels** -- `should_log?` and the `semantic_logger_filter` are native-only.
|
|
108
|
+
2. **`string_list` configs** -- accessed via `fetch_object_value` and used as `Array<String>`.
|
|
109
|
+
3. **`duration` configs** -- exposed only via the native `get_duration` (returns milliseconds).
|
|
110
|
+
4. **`bytes` configs** -- not accessible via OpenFeature (no binary type in OF).
|
|
111
|
+
5. **`keys` / raw config inspection** -- native-only via `provider.client`.
|
|
112
|
+
6. **Context keys use dot-notation** -- `"user.email"`, not nested hashes.
|
|
113
|
+
7. **`targeting_key` maps to `user.id` by default** -- configure
|
|
114
|
+
`targeting_key_mapping` if you target a different namespace.
|
|
115
|
+
|
|
116
|
+
## Offline / test mode
|
|
117
|
+
|
|
118
|
+
Pass `datadir:` to evaluate against a Quonfig workspace on disk -- the same
|
|
119
|
+
layout the integration test suite uses. No network or SDK key is required.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
provider = Quonfig::OpenFeature::Provider.new(
|
|
123
|
+
datadir: '/path/to/workspace',
|
|
124
|
+
environment: 'Production',
|
|
125
|
+
enable_sse: false
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
bundle install
|
|
133
|
+
bundle exec rake test
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The development `Gemfile` references the sibling `../sdk-ruby/` checkout via a
|
|
137
|
+
`path:` reference; downstream consumers depend on the `quonfig` gem from
|
|
138
|
+
RubyGems.
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.1
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quonfig
|
|
4
|
+
module OpenFeature
|
|
5
|
+
# Maps an OpenFeature flat EvaluationContext to Quonfig's nested contexts hash.
|
|
6
|
+
#
|
|
7
|
+
# Mapping rules (mirror @quonfig/openfeature-node and openfeature-go):
|
|
8
|
+
# - +targeting_key+ maps to namespace+property specified by +targeting_key_mapping+
|
|
9
|
+
# (default "user.id"). The mapping is split on the FIRST dot — namespace before,
|
|
10
|
+
# property after.
|
|
11
|
+
# - Keys with a dot are split on the first dot: "user.email" -> { "user" => { "email" => v } }
|
|
12
|
+
# - Keys without a dot go to the default empty-string namespace:
|
|
13
|
+
# "country" -> { "" => { "country" => v } }
|
|
14
|
+
# - Multi-dot keys split on the first dot only:
|
|
15
|
+
# "user.ip.address" -> { "user" => { "ip.address" => v } }
|
|
16
|
+
# - Nil values are skipped.
|
|
17
|
+
# - An empty / nil context returns +{}+.
|
|
18
|
+
module Context
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
DEFAULT_TARGETING_KEY_MAPPING = 'user.id'
|
|
22
|
+
TARGETING_KEY_FIELDS = %w[targeting_key targetingKey].freeze
|
|
23
|
+
|
|
24
|
+
# @param of_context [::OpenFeature::SDK::EvaluationContext, Hash, nil]
|
|
25
|
+
# @param targeting_key_mapping [String]
|
|
26
|
+
# @return [Hash{String=>Hash{String=>Object}}]
|
|
27
|
+
def map_context(of_context, targeting_key_mapping = DEFAULT_TARGETING_KEY_MAPPING)
|
|
28
|
+
return {} if of_context.nil?
|
|
29
|
+
|
|
30
|
+
fields, targeting_key = extract_fields_and_targeting_key(of_context)
|
|
31
|
+
return {} if fields.empty? && (targeting_key.nil? || targeting_key.to_s.empty?)
|
|
32
|
+
|
|
33
|
+
result = {}
|
|
34
|
+
|
|
35
|
+
unless targeting_key.nil? || targeting_key.to_s.empty?
|
|
36
|
+
ns, prop = split_first(targeting_key_mapping)
|
|
37
|
+
assign(result, ns, prop, targeting_key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
fields.each do |key, value|
|
|
41
|
+
next if value.nil?
|
|
42
|
+
# The targeting_key is handled above; do not also write it under its raw name.
|
|
43
|
+
next if TARGETING_KEY_FIELDS.include?(key.to_s)
|
|
44
|
+
|
|
45
|
+
ns, prop = split_first(key.to_s)
|
|
46
|
+
assign(result, ns, prop, value)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns [fields_hash_with_string_keys, targeting_key_or_nil]. Accepts an
|
|
53
|
+
# OpenFeature::SDK::EvaluationContext (preferred) or a plain Hash.
|
|
54
|
+
def extract_fields_and_targeting_key(ctx)
|
|
55
|
+
if ctx.respond_to?(:fields) && ctx.respond_to?(:targeting_key)
|
|
56
|
+
[(ctx.fields || {}).transform_keys(&:to_s), ctx.targeting_key]
|
|
57
|
+
elsif ctx.is_a?(Hash)
|
|
58
|
+
stringified = ctx.transform_keys(&:to_s)
|
|
59
|
+
tk = stringified['targeting_key'] || stringified['targetingKey']
|
|
60
|
+
[stringified, tk]
|
|
61
|
+
else
|
|
62
|
+
[{}, nil]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def split_first(key)
|
|
67
|
+
idx = key.index('.')
|
|
68
|
+
return ['', key] if idx.nil?
|
|
69
|
+
|
|
70
|
+
[key[0...idx], key[(idx + 1)..]]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def assign(result, namespace, property, value)
|
|
74
|
+
result[namespace] ||= {}
|
|
75
|
+
result[namespace][property] = value
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open_feature/sdk'
|
|
4
|
+
|
|
5
|
+
module Quonfig
|
|
6
|
+
module OpenFeature
|
|
7
|
+
# Maps native Quonfig SDK errors to OpenFeature ErrorCode constants.
|
|
8
|
+
module Errors
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
ErrorCode = ::OpenFeature::SDK::Provider::ErrorCode
|
|
12
|
+
|
|
13
|
+
# @param err [Exception, String, nil]
|
|
14
|
+
# @return [String] one of the ErrorCode constants
|
|
15
|
+
def to_error_code(err)
|
|
16
|
+
return ErrorCode::GENERAL if err.nil?
|
|
17
|
+
|
|
18
|
+
# Class-based mapping is the most reliable signal.
|
|
19
|
+
if defined?(::Quonfig::Errors::MissingDefaultError) && err.is_a?(::Quonfig::Errors::MissingDefaultError)
|
|
20
|
+
return ErrorCode::FLAG_NOT_FOUND
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if defined?(::Quonfig::Errors::TypeMismatchError) && err.is_a?(::Quonfig::Errors::TypeMismatchError)
|
|
24
|
+
return ErrorCode::TYPE_MISMATCH
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if (defined?(::Quonfig::Errors::UninitializedError) && err.is_a?(::Quonfig::Errors::UninitializedError)) ||
|
|
28
|
+
(defined?(::Quonfig::Errors::InitializationTimeoutError) && err.is_a?(::Quonfig::Errors::InitializationTimeoutError))
|
|
29
|
+
return ErrorCode::PROVIDER_NOT_READY
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Fallback: inspect the message text for keywords, matching the Node provider.
|
|
33
|
+
msg = (err.respond_to?(:message) ? err.message : err.to_s).to_s.downcase
|
|
34
|
+
return ErrorCode::FLAG_NOT_FOUND if msg.include?('not found') ||
|
|
35
|
+
msg.include?('no value found') ||
|
|
36
|
+
msg.include?('value found for key')
|
|
37
|
+
return ErrorCode::TYPE_MISMATCH if msg.include?('type mismatch') ||
|
|
38
|
+
msg.include?('expected ') && msg.include?('got ')
|
|
39
|
+
return ErrorCode::PROVIDER_NOT_READY if msg.include?('not initialized') ||
|
|
40
|
+
msg.include?('provider not ready') ||
|
|
41
|
+
msg.include?("couldn't initialize") ||
|
|
42
|
+
msg.include?('initialization timeout')
|
|
43
|
+
|
|
44
|
+
ErrorCode::GENERAL
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open_feature/sdk'
|
|
4
|
+
require 'quonfig'
|
|
5
|
+
|
|
6
|
+
require 'quonfig/openfeature/context'
|
|
7
|
+
require 'quonfig/openfeature/errors'
|
|
8
|
+
|
|
9
|
+
module Quonfig
|
|
10
|
+
module OpenFeature
|
|
11
|
+
# OpenFeature provider that wraps the +quonfig+ Ruby SDK and implements the
|
|
12
|
+
# OpenFeature server-side provider contract:
|
|
13
|
+
#
|
|
14
|
+
# * +metadata+
|
|
15
|
+
# * +init(evaluation_context = nil)+
|
|
16
|
+
# * +shutdown+
|
|
17
|
+
# * +fetch_boolean_value(flag_key:, default_value:, evaluation_context:)+
|
|
18
|
+
# * +fetch_string_value(flag_key:, default_value:, evaluation_context:)+
|
|
19
|
+
# * +fetch_number_value(flag_key:, default_value:, evaluation_context:)+
|
|
20
|
+
# * +fetch_integer_value(flag_key:, default_value:, evaluation_context:)+
|
|
21
|
+
# * +fetch_float_value(flag_key:, default_value:, evaluation_context:)+
|
|
22
|
+
# * +fetch_object_value(flag_key:, default_value:, evaluation_context:)+
|
|
23
|
+
#
|
|
24
|
+
# Usage:
|
|
25
|
+
#
|
|
26
|
+
# require 'quonfig/openfeature'
|
|
27
|
+
# require 'open_feature/sdk'
|
|
28
|
+
#
|
|
29
|
+
# provider = Quonfig::OpenFeature::Provider.new(sdk_key: 'qf_sk_...')
|
|
30
|
+
# OpenFeature::SDK.set_provider_and_wait(provider)
|
|
31
|
+
#
|
|
32
|
+
# client = OpenFeature::SDK.build_client
|
|
33
|
+
# client.fetch_boolean_value(flag_key: 'my-flag', default_value: false)
|
|
34
|
+
class Provider
|
|
35
|
+
NAME = 'quonfig'
|
|
36
|
+
|
|
37
|
+
ResolutionDetails = ::OpenFeature::SDK::Provider::ResolutionDetails
|
|
38
|
+
Reason = ::OpenFeature::SDK::Provider::Reason
|
|
39
|
+
ErrorCode = ::OpenFeature::SDK::Provider::ErrorCode
|
|
40
|
+
ProviderMetadata = ::OpenFeature::SDK::Provider::ProviderMetadata
|
|
41
|
+
|
|
42
|
+
attr_reader :metadata, :targeting_key_mapping
|
|
43
|
+
|
|
44
|
+
# @param sdk_key [String, nil] SDK key for the live delivery service.
|
|
45
|
+
# @param datadir [String, nil] path to a Quonfig workspace for offline mode.
|
|
46
|
+
# @param environment [String, nil] which environment to evaluate.
|
|
47
|
+
# @param targeting_key_mapping [String] dot-notation path the OpenFeature
|
|
48
|
+
# targeting_key is rewritten to (default "user.id").
|
|
49
|
+
# @param client [Quonfig::Client, nil] inject a pre-built Quonfig client
|
|
50
|
+
# (primarily for tests). When supplied, the other +sdk_key+/+datadir+/etc.
|
|
51
|
+
# options are ignored.
|
|
52
|
+
# @param quonfig_options [Hash] any other keyword arguments are forwarded
|
|
53
|
+
# verbatim to +Quonfig::Client.new+.
|
|
54
|
+
def initialize(sdk_key: nil, datadir: nil, environment: nil,
|
|
55
|
+
targeting_key_mapping: Context::DEFAULT_TARGETING_KEY_MAPPING,
|
|
56
|
+
client: nil, **quonfig_options)
|
|
57
|
+
@metadata = ProviderMetadata.new(name: NAME).freeze
|
|
58
|
+
@targeting_key_mapping = targeting_key_mapping
|
|
59
|
+
@client = client
|
|
60
|
+
@quonfig_options = build_quonfig_options(
|
|
61
|
+
sdk_key: sdk_key,
|
|
62
|
+
datadir: datadir,
|
|
63
|
+
environment: environment,
|
|
64
|
+
extra: quonfig_options
|
|
65
|
+
)
|
|
66
|
+
@initialized = !@client.nil?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Initialize the underlying Quonfig client. Called by
|
|
70
|
+
# +OpenFeature::SDK.set_provider_and_wait+.
|
|
71
|
+
def init(_evaluation_context = nil)
|
|
72
|
+
return if @initialized
|
|
73
|
+
|
|
74
|
+
@client = ::Quonfig::Client.new(@quonfig_options)
|
|
75
|
+
@initialized = true
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Shut the provider down. Mirrors the OpenFeature InMemoryProvider
|
|
80
|
+
# contract — silently no-ops if the client was never built.
|
|
81
|
+
def shutdown
|
|
82
|
+
client = @client
|
|
83
|
+
@client = nil
|
|
84
|
+
@initialized = false
|
|
85
|
+
client&.stop
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Escape hatch: returns the underlying +Quonfig::Client+ for native-only
|
|
90
|
+
# features (keys, raw config, durations, log levels). Returns +nil+ until
|
|
91
|
+
# +init+ has run.
|
|
92
|
+
def client
|
|
93
|
+
@client
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# ---- fetch_*_value -----------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
99
|
+
evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
|
|
100
|
+
value = client.get_bool(flag_key, default: nil, context: mapped_ctx)
|
|
101
|
+
if value.nil?
|
|
102
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
103
|
+
error_code: ErrorCode::FLAG_NOT_FOUND,
|
|
104
|
+
error_message: "flag not found: #{flag_key}")
|
|
105
|
+
else
|
|
106
|
+
ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
|
112
|
+
evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
|
|
113
|
+
value = client.get_string(flag_key, default: nil, context: mapped_ctx)
|
|
114
|
+
if value.nil?
|
|
115
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
116
|
+
error_code: ErrorCode::FLAG_NOT_FOUND,
|
|
117
|
+
error_message: "flag not found: #{flag_key}")
|
|
118
|
+
else
|
|
119
|
+
ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
|
125
|
+
# OpenFeature's "number" is Ruby Numeric (Integer or Float). Try integer
|
|
126
|
+
# first, fall back to float so we transparently handle both Quonfig
|
|
127
|
+
# int and double configs.
|
|
128
|
+
evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
|
|
129
|
+
value = nil
|
|
130
|
+
begin
|
|
131
|
+
value = client.get_int(flag_key, default: nil, context: mapped_ctx)
|
|
132
|
+
rescue ::Quonfig::Errors::TypeMismatchError
|
|
133
|
+
value = client.get_float(flag_key, default: nil, context: mapped_ctx)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if value.nil?
|
|
137
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
138
|
+
error_code: ErrorCode::FLAG_NOT_FOUND,
|
|
139
|
+
error_message: "flag not found: #{flag_key}")
|
|
140
|
+
else
|
|
141
|
+
ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
|
|
147
|
+
evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
|
|
148
|
+
value = client.get_int(flag_key, default: nil, context: mapped_ctx)
|
|
149
|
+
if value.nil?
|
|
150
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
151
|
+
error_code: ErrorCode::FLAG_NOT_FOUND,
|
|
152
|
+
error_message: "flag not found: #{flag_key}")
|
|
153
|
+
else
|
|
154
|
+
ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
|
|
160
|
+
evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
|
|
161
|
+
value = client.get_float(flag_key, default: nil, context: mapped_ctx)
|
|
162
|
+
if value.nil?
|
|
163
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
164
|
+
error_code: ErrorCode::FLAG_NOT_FOUND,
|
|
165
|
+
error_message: "flag not found: #{flag_key}")
|
|
166
|
+
else
|
|
167
|
+
ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Object resolution tries +get_string_list+ first (so Quonfig
|
|
173
|
+
# +string_list+ configs surface as native arrays), then falls back to
|
|
174
|
+
# +get_json+ for any other JSON-shaped config.
|
|
175
|
+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
|
176
|
+
evaluate(flag_key, default_value, evaluation_context) do |client, mapped_ctx|
|
|
177
|
+
value = nil
|
|
178
|
+
begin
|
|
179
|
+
value = client.get_string_list(flag_key, default: nil, context: mapped_ctx)
|
|
180
|
+
rescue ::Quonfig::Errors::TypeMismatchError
|
|
181
|
+
value = nil
|
|
182
|
+
end
|
|
183
|
+
if value.nil?
|
|
184
|
+
value = client.get_json(flag_key, default: nil, context: mapped_ctx)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if value.nil?
|
|
188
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
189
|
+
error_code: ErrorCode::FLAG_NOT_FOUND,
|
|
190
|
+
error_message: "flag not found: #{flag_key}")
|
|
191
|
+
else
|
|
192
|
+
ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
def evaluate(flag_key, default_value, evaluation_context)
|
|
200
|
+
client = @client
|
|
201
|
+
if client.nil?
|
|
202
|
+
return ResolutionDetails.new(
|
|
203
|
+
value: default_value,
|
|
204
|
+
reason: Reason::ERROR,
|
|
205
|
+
error_code: ErrorCode::PROVIDER_NOT_READY,
|
|
206
|
+
error_message: 'Quonfig provider has not been initialized'
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
mapped_ctx = Context.map_context(evaluation_context, @targeting_key_mapping)
|
|
211
|
+
yield(client, mapped_ctx)
|
|
212
|
+
rescue ::Quonfig::Errors::MissingDefaultError => e
|
|
213
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
214
|
+
error_code: ErrorCode::FLAG_NOT_FOUND,
|
|
215
|
+
error_message: e.message)
|
|
216
|
+
rescue ::Quonfig::Errors::TypeMismatchError => e
|
|
217
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
218
|
+
error_code: ErrorCode::TYPE_MISMATCH,
|
|
219
|
+
error_message: e.message)
|
|
220
|
+
rescue ::Quonfig::Errors::UninitializedError, ::Quonfig::Errors::InitializationTimeoutError => e
|
|
221
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
222
|
+
error_code: ErrorCode::PROVIDER_NOT_READY,
|
|
223
|
+
error_message: e.message)
|
|
224
|
+
rescue StandardError => e
|
|
225
|
+
ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
|
|
226
|
+
error_code: Errors.to_error_code(e),
|
|
227
|
+
error_message: e.message)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def build_quonfig_options(sdk_key:, datadir:, environment:, extra:)
|
|
231
|
+
opts = {}
|
|
232
|
+
opts[:sdk_key] = sdk_key unless sdk_key.nil?
|
|
233
|
+
opts[:datadir] = datadir unless datadir.nil?
|
|
234
|
+
opts[:environment] = environment unless environment.nil?
|
|
235
|
+
opts.merge(extra)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: quonfig-openfeature
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jeff Dwyer
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: openfeature-sdk
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.5'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.5'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: quonfig
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.0.8
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 0.0.8
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: minitest
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '5.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '5.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: minitest-reporters
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rake
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '13.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '13.0'
|
|
83
|
+
description: OpenFeature provider that wraps the quonfig Ruby SDK and implements the
|
|
84
|
+
OpenFeature Ruby provider contract.
|
|
85
|
+
email:
|
|
86
|
+
- jeff@quonfig.com
|
|
87
|
+
executables: []
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- LICENSE.txt
|
|
92
|
+
- README.md
|
|
93
|
+
- VERSION
|
|
94
|
+
- lib/quonfig/openfeature.rb
|
|
95
|
+
- lib/quonfig/openfeature/context.rb
|
|
96
|
+
- lib/quonfig/openfeature/errors.rb
|
|
97
|
+
- lib/quonfig/openfeature/provider.rb
|
|
98
|
+
- lib/quonfig/openfeature/version.rb
|
|
99
|
+
homepage: https://github.com/quonfig/openfeature-ruby
|
|
100
|
+
licenses:
|
|
101
|
+
- MIT
|
|
102
|
+
metadata: {}
|
|
103
|
+
post_install_message:
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '3.0'
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.5.22
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: OpenFeature provider for Quonfig (Ruby)
|
|
122
|
+
test_files: []
|