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 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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quonfig
4
+ module OpenFeature
5
+ VERSION = File.read(File.expand_path('../../../VERSION', __dir__)).strip
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open_feature/sdk'
4
+ require 'quonfig'
5
+
6
+ require 'quonfig/openfeature/version'
7
+ require 'quonfig/openfeature/errors'
8
+ require 'quonfig/openfeature/context'
9
+ require 'quonfig/openfeature/provider'
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: []