openfeature-sdk 0.6.2 → 0.6.3

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: 304772054cf10c0cc02870fdeb7a1a7412342912e3f8a08d80c33fe25bd99849
4
- data.tar.gz: 6a27c2a8bf23074292594f4eec0ecd4dd9df11ddc1e3e48c4c7ff245e76a6381
3
+ metadata.gz: bd3ec1b654a844eaa7bcafe400d6d2075199fa49ba6823aad1c8e1ce8c05a526
4
+ data.tar.gz: 3d159cb4f4df832154cfa7a4657ee53f357827a3d55dcbe124bf4716248e24b8
5
5
  SHA512:
6
- metadata.gz: bcaacd882ecdf0ebd0643e1a839bae3d4efb02deb30f2128c5feca91c93e85bee65b755147cf4bef6edabc010875746b777cd4570f26161451a620db396e02e8
7
- data.tar.gz: 5a6ba82992f4d8e960d350a573e48a5e015c9b8eb953737354a4573d0a9f89ca6c4615553d0240a91c1490aad8fc3e1c2ca4bfeddc64d0b1203835ede55e1c77
6
+ metadata.gz: 66667a224162e00b882ad73b45e5d2825cc35f47007ca961749cc7115cbe1223d881be0cfff89eb4a5d3fd3f0235c2548f7a89420de9ff7bb3a6b0fd4bcada65
7
+ data.tar.gz: f799ac085e5593bc9b14a040ebddc8326c57ff46e36285ed0ea9b44008dd0d8d58a149a1c0f7fc9be50aeb3cf8d60ffc2859f2b3f9099d49e1db3df6f795b669
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.6.2"
2
+ ".": "0.6.3"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.3](https://github.com/open-feature/ruby-sdk/compare/v0.6.2...v0.6.3) (2026-03-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * close spec compliance gaps for OpenFeature v0.8.0 ([#237](https://github.com/open-feature/ruby-sdk/issues/237)) ([9a87d04](https://github.com/open-feature/ruby-sdk/commit/9a87d04d5f261ea06e073f405c15613db7099d8a))
9
+ * enable Gherkin feature tests ([#50](https://github.com/open-feature/ruby-sdk/issues/50)) ([#233](https://github.com/open-feature/ruby-sdk/issues/233)) ([95845ba](https://github.com/open-feature/ruby-sdk/commit/95845ba6ec26357d9c0895d310361e411f85da11))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * close remaining MUST-level spec compliance gaps ([#238](https://github.com/open-feature/ruby-sdk/issues/238)) ([1d08491](https://github.com/open-feature/ruby-sdk/commit/1d084911964c8672dd66b23834eec6f14e453749))
15
+
3
16
  ## [0.6.2](https://github.com/open-feature/ruby-sdk/compare/v0.6.1...v0.6.2) (2026-03-07)
4
17
 
5
18
 
data/Gemfile CHANGED
@@ -4,3 +4,6 @@ source "https://rubygems.org"
4
4
 
5
5
  # Specify your gem's dependencies in openfeature-sdk.gemspec
6
6
  gemspec
7
+
8
+ gem "cucumber", "~> 10.0", group: :test
9
+ gem "logger", group: :test
data/Gemfile.lock CHANGED
@@ -1,12 +1,40 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.6.2)
4
+ openfeature-sdk (0.6.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  ast (2.4.3)
10
+ base64 (0.3.0)
11
+ bigdecimal (4.0.1)
12
+ builder (3.3.0)
13
+ cucumber (10.2.0)
14
+ base64 (~> 0.2)
15
+ builder (~> 3.2)
16
+ cucumber-ci-environment (> 9, < 12)
17
+ cucumber-core (> 15, < 17)
18
+ cucumber-cucumber-expressions (> 17, < 20)
19
+ cucumber-html-formatter (> 21, < 23)
20
+ diff-lcs (~> 1.5)
21
+ logger (~> 1.6)
22
+ mini_mime (~> 1.1)
23
+ multi_test (~> 1.1)
24
+ sys-uname (~> 1.3)
25
+ cucumber-ci-environment (11.0.0)
26
+ cucumber-core (16.2.0)
27
+ cucumber-gherkin (> 36, < 40)
28
+ cucumber-messages (> 31, < 33)
29
+ cucumber-tag-expressions (> 6, < 9)
30
+ cucumber-cucumber-expressions (19.0.0)
31
+ bigdecimal
32
+ cucumber-gherkin (39.0.0)
33
+ cucumber-messages (>= 31, < 33)
34
+ cucumber-html-formatter (22.3.0)
35
+ cucumber-messages (> 23, < 33)
36
+ cucumber-messages (32.2.0)
37
+ cucumber-tag-expressions (8.1.0)
10
38
  date (3.5.1)
11
39
  debug (1.11.1)
12
40
  irb (~> 1.10)
@@ -14,6 +42,7 @@ GEM
14
42
  diff-lcs (1.6.2)
15
43
  docile (1.4.1)
16
44
  erb (6.0.2)
45
+ ffi (1.17.3)
17
46
  io-console (0.8.2)
18
47
  irb (1.17.0)
19
48
  pp (>= 0.6.0)
@@ -23,7 +52,11 @@ GEM
23
52
  json (2.18.1)
24
53
  language_server-protocol (3.17.0.5)
25
54
  lint_roller (1.1.0)
55
+ logger (1.7.0)
26
56
  markly (0.15.2)
57
+ memoist3 (1.0.0)
58
+ mini_mime (1.1.5)
59
+ multi_test (1.1.0)
27
60
  parallel (1.27.0)
28
61
  parser (3.3.10.2)
29
62
  ast (~> 2.4.1)
@@ -100,6 +133,9 @@ GEM
100
133
  lint_roller (~> 1.1)
101
134
  rubocop-performance (~> 1.26.0)
102
135
  stringio (3.2.0)
136
+ sys-uname (1.5.0)
137
+ ffi (~> 1.1)
138
+ memoist3 (~> 1.0.0)
103
139
  timecop (0.9.10)
104
140
  tsort (0.2.0)
105
141
  unicode-display_width (3.2.0)
@@ -120,7 +156,9 @@ PLATFORMS
120
156
  x86_64-linux
121
157
 
122
158
  DEPENDENCIES
159
+ cucumber (~> 10.0)
123
160
  debug
161
+ logger
124
162
  markly
125
163
  openfeature-sdk!
126
164
  rake (~> 13.0)
data/README.md CHANGED
@@ -17,8 +17,8 @@
17
17
  </a>
18
18
  <!-- x-release-please-start-version -->
19
19
 
20
- <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.6.2">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.2&color=blue&style=for-the-badge" />
20
+ <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.6.3">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.3&color=blue&style=for-the-badge" />
22
22
  </a>
23
23
 
24
24
  <!-- x-release-please-end -->
data/Rakefile CHANGED
@@ -7,4 +7,9 @@ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  require "standard/rake"
9
9
 
10
+ desc "Run Cucumber Gherkin feature tests"
11
+ task :cucumber do
12
+ sh "bundle exec cucumber"
13
+ end
14
+
10
15
  task default: %i[spec standard]
data/cucumber.yml ADDED
@@ -0,0 +1,2 @@
1
+ default: spec/open-feature-spec/specification/assets/gherkin --require features --tags "not @deprecated" --publish-quiet
2
+ deprecated: spec/open-feature-spec/specification/assets/gherkin --require features --publish-quiet
@@ -34,7 +34,7 @@ module OpenFeature
34
34
  include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1
35
35
  extend Forwardable
36
36
 
37
- def_delegators :configuration, :provider, :set_provider, :set_provider_and_wait, :hooks, :evaluation_context
37
+ def_delegators :configuration, :provider, :set_provider, :set_provider_and_wait, :hooks, :add_hooks, :evaluation_context
38
38
 
39
39
  def configuration
40
40
  @configuration ||= Configuration.new
@@ -54,6 +54,11 @@ module OpenFeature
54
54
  Client.new(provider: Provider::NoOpProvider.new, evaluation_context:)
55
55
  end
56
56
 
57
+ def provider_metadata(domain: nil)
58
+ prov = provider(domain: domain)
59
+ prov.metadata if prov&.respond_to?(:metadata)
60
+ end
61
+
57
62
  def add_handler(event_type, handler)
58
63
  configuration.add_handler(event_type, handler)
59
64
  end
@@ -28,6 +28,10 @@ module OpenFeature
28
28
  @hooks = []
29
29
  end
30
30
 
31
+ def add_hooks(*new_hooks)
32
+ @hooks.concat(new_hooks.flatten)
33
+ end
34
+
31
35
  def provider_status
32
36
  OpenFeature::SDK.configuration.provider_state(@provider)
33
37
  end
@@ -70,27 +74,40 @@ module OpenFeature
70
74
  def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
71
75
  validate_default_value_type(type, default_value)
72
76
 
73
- if OpenFeature::SDK.configuration.provider_tracked?(@provider)
74
- error_code = short_circuit_error_code(provider_status)
75
- if error_code
76
- resolution = Provider::ResolutionDetails.new(
77
- value: default_value,
78
- error_code: error_code,
79
- reason: Provider::Reason::ERROR
80
- )
81
- return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
82
- end
83
- end
84
-
85
77
  built_context = build_evaluation_context(evaluation_context)
86
78
 
87
79
  # Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
88
80
  provider_hooks = @provider.respond_to?(:hooks) ? Array(@provider.hooks) : []
89
81
  ordered_hooks = [*OpenFeature::SDK.hooks, *@hooks, *invocation_hooks, *provider_hooks]
90
82
 
83
+ # Check for short-circuit conditions (spec 1.7.6 + 1.7.7)
84
+ short_circuit_code = nil
85
+ if OpenFeature::SDK.configuration.provider_tracked?(@provider)
86
+ short_circuit_code = short_circuit_error_code(provider_status)
87
+ end
88
+
91
89
  # Fast path: skip hook ceremony when no hooks are registered
92
90
  if ordered_hooks.empty?
93
- return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
91
+ if short_circuit_code
92
+ resolution = Provider::ResolutionDetails.new(
93
+ value: default_value,
94
+ error_code: short_circuit_code,
95
+ reason: Provider::Reason::ERROR
96
+ )
97
+ return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
98
+ end
99
+
100
+ begin
101
+ return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
102
+ rescue => e
103
+ resolution = Provider::ResolutionDetails.new(
104
+ value: default_value,
105
+ error_code: Provider::ErrorCode::GENERAL,
106
+ reason: Provider::Reason::ERROR,
107
+ error_message: e.message
108
+ )
109
+ return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
110
+ end
94
111
  end
95
112
 
96
113
  hook_context = Hooks::HookContext.new(
@@ -111,8 +128,21 @@ module OpenFeature
111
128
  end
112
129
 
113
130
  executor = Hooks::HookExecutor.new(logger: OpenFeature::SDK.configuration.logger)
114
- executor.execute(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints) do |hctx|
115
- evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
131
+
132
+ if short_circuit_code
133
+ # Spec 1.7.6 + 1.7.7: short-circuit must still run error hooks and finally hooks
134
+ resolution = Provider::ResolutionDetails.new(
135
+ value: default_value,
136
+ error_code: short_circuit_code,
137
+ reason: Provider::Reason::ERROR
138
+ )
139
+ evaluation_details = EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
140
+ executor.run_short_circuit(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints, evaluation_details: evaluation_details)
141
+ evaluation_details
142
+ else
143
+ executor.execute(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints) do |hctx|
144
+ evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
145
+ end
116
146
  end
117
147
  end
118
148
 
@@ -128,6 +158,7 @@ module OpenFeature
128
158
  resolution_details.value = default_value
129
159
  resolution_details.error_code = Provider::ErrorCode::TYPE_MISMATCH
130
160
  resolution_details.reason = Provider::Reason::ERROR
161
+ resolution_details.variant = nil
131
162
  end
132
163
 
133
164
  EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
@@ -2,6 +2,8 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- ClientMetadata = Struct.new(:domain, keyword_init: true)
5
+ ClientMetadata = Struct.new(:domain, keyword_init: true) do
6
+ alias_method :name, :domain
7
+ end
6
8
  end
7
9
  end
@@ -31,6 +31,10 @@ module OpenFeature
31
31
  @client_handlers_mutex = Mutex.new
32
32
  end
33
33
 
34
+ def add_hooks(*new_hooks)
35
+ @hooks.concat(new_hooks.flatten)
36
+ end
37
+
34
38
  def provider(domain: nil)
35
39
  @providers[domain] || @providers[nil]
36
40
  end
@@ -85,9 +89,11 @@ module OpenFeature
85
89
  end
86
90
 
87
91
  def shutdown
88
- providers_to_shutdown = @provider_mutex.synchronize { @providers.values.uniq }
92
+ providers_to_shutdown = @provider_mutex.synchronize { @providers.values.uniq(&:object_id) }
89
93
 
90
94
  providers_to_shutdown.each do |prov|
95
+ # Spec 1.7.9: Set provider state to NOT_READY before shutdown
96
+ @provider_state_registry.set_initial_state(prov, ProviderState::NOT_READY)
91
97
  prov.shutdown if prov.respond_to?(:shutdown)
92
98
  rescue => e
93
99
  @logger&.warn("Error shutting down provider #{prov&.class&.name || "unknown"}: #{e.message}")
@@ -107,34 +113,46 @@ module OpenFeature
107
113
  @provider_mutex.synchronize do
108
114
  @providers.clear
109
115
  end
116
+ @hooks.clear
117
+ @evaluation_context = nil
118
+ @transaction_context_propagator = nil
110
119
  end
111
120
 
112
121
  def set_provider_internal(provider, domain:, wait_for_init:)
113
122
  # Capture evaluation context before acquiring mutex to prevent race conditions
114
123
  context_for_init = @evaluation_context
115
124
 
116
- old_provider, provider_to_init = nil
125
+ old_provider = nil
126
+ needs_init = false
127
+ needs_shutdown = false
117
128
 
118
129
  @provider_mutex.synchronize do
119
130
  old_provider = @providers[domain]
120
131
 
121
- # Remove old provider state to prevent memory leaks
122
- @provider_state_registry.remove_provider(old_provider)
123
-
124
132
  new_providers = @providers.dup
125
133
  new_providers[domain] = provider
126
134
  @providers = new_providers
127
135
 
128
- @provider_state_registry.set_initial_state(provider)
136
+ # Spec 1.1.2.2: Only initialize if the provider is not already active
137
+ # (i.e., not already bound to another domain)
138
+ already_active = @providers.any? { |d, p| d != domain && p.equal?(provider) && @provider_state_registry.tracked?(p) }
139
+ needs_init = !already_active
129
140
 
130
- provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
141
+ if needs_init
142
+ @provider_state_registry.set_initial_state(provider)
143
+ provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
144
+ end
131
145
 
132
- provider_to_init = provider
146
+ # Spec 1.1.2.3: Only shutdown old provider if it's no longer bound to any domain
147
+ if old_provider && !old_provider.equal?(provider)
148
+ still_bound = @providers.any? { |_, p| p.equal?(old_provider) }
149
+ needs_shutdown = !still_bound
150
+ @provider_state_registry.remove_provider(old_provider) unless still_bound
151
+ end
133
152
  end
134
153
 
135
154
  # Shutdown old provider outside mutex to avoid blocking other operations
136
- # Only shutdown if it's a different provider to prevent race condition
137
- if old_provider && old_provider != provider
155
+ if needs_shutdown
138
156
  begin
139
157
  old_provider.shutdown if old_provider.respond_to?(:shutdown)
140
158
  rescue => e
@@ -143,12 +161,17 @@ module OpenFeature
143
161
  end
144
162
 
145
163
  # Initialize provider outside the mutex to avoid blocking other operations
146
- if wait_for_init
147
- init_provider(provider_to_init, context_for_init, raise_on_error: true)
148
- else
149
- Thread.new do
150
- init_provider(provider_to_init, context_for_init, raise_on_error: false)
164
+ if needs_init
165
+ if wait_for_init
166
+ init_provider(provider, context_for_init, raise_on_error: true)
167
+ else
168
+ Thread.new do
169
+ init_provider(provider, context_for_init, raise_on_error: false)
170
+ end
151
171
  end
172
+ elsif wait_for_init
173
+ # Provider already active; no init needed but still dispatch READY
174
+ dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
152
175
  end
153
176
  end
154
177
 
@@ -164,16 +187,22 @@ module OpenFeature
164
187
 
165
188
  dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
166
189
  rescue => e
190
+ # Spec 1.7.8: Propagate error code from provider if available
191
+ error_code = if e.respond_to?(:error_code) && e.error_code
192
+ e.error_code
193
+ else
194
+ Provider::ErrorCode::GENERAL
195
+ end
196
+
167
197
  dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
168
- error_code: Provider::ErrorCode::GENERAL,
198
+ error_code: error_code,
169
199
  message: e.message)
170
200
 
171
201
  if raise_on_error
172
- # Re-raise as ProviderInitializationError for synchronous callers
173
202
  raise ProviderInitializationError.new(
174
203
  "Provider #{provider.class.name} initialization failed: #{e.message}",
175
204
  provider:,
176
- error_code: Provider::ErrorCode::GENERAL,
205
+ error_code: error_code,
177
206
  original_error: e
178
207
  )
179
208
  end
@@ -18,6 +18,20 @@ module OpenFeature
18
18
  @logger = logger
19
19
  end
20
20
 
21
+ # Runs error hooks and finally hooks for a short-circuit evaluation
22
+ # (spec 1.7.6 + 1.7.7). Before hooks and after hooks are NOT run.
23
+ #
24
+ # @param ordered_hooks [Array] hooks in before-order
25
+ # @param hook_context [HookContext] the hook context
26
+ # @param hints [Hints] hook hints
27
+ # @param evaluation_details [EvaluationDetails] the short-circuit result
28
+ def run_short_circuit(ordered_hooks:, hook_context:, hints:, evaluation_details:)
29
+ error = StandardError.new(evaluation_details.error_code)
30
+ run_error_hooks(ordered_hooks, hook_context, error, hints)
31
+ ensure
32
+ run_finally_hooks(ordered_hooks, hook_context, evaluation_details, hints)
33
+ end
34
+
21
35
  # Executes the full hook lifecycle around the flag evaluation block.
22
36
  #
23
37
  # @param ordered_hooks [Array] hooks in before-order (API, Client, Invocation, Provider)
@@ -31,7 +45,15 @@ module OpenFeature
31
45
  begin
32
46
  run_before_hooks(ordered_hooks, hook_context, hints)
33
47
  evaluation_details = evaluate_block.call(hook_context)
34
- run_after_hooks(ordered_hooks, hook_context, evaluation_details, hints)
48
+
49
+ # Spec 4.3.6: If evaluation resulted in an error (e.g. FLAG_NOT_FOUND,
50
+ # TYPE_MISMATCH), run error hooks instead of after hooks.
51
+ if evaluation_details.error_code
52
+ error = StandardError.new(evaluation_details.error_message || evaluation_details.error_code)
53
+ run_error_hooks(ordered_hooks, hook_context, error, hints)
54
+ else
55
+ run_after_hooks(ordered_hooks, hook_context, evaluation_details, hints)
56
+ end
35
57
  rescue => e
36
58
  run_error_hooks(ordered_hooks, hook_context, e, hints)
37
59
 
@@ -29,8 +29,10 @@ module OpenFeature
29
29
  end
30
30
 
31
31
  def update_flags(new_flags)
32
+ old_keys = @flags.keys
32
33
  @flags = new_flags.dup
33
- emit_provider_changed(new_flags.keys)
34
+ changed_keys = (old_keys | new_flags.keys)
35
+ emit_provider_changed(changed_keys)
34
36
  end
35
37
 
36
38
  def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.6.2"
5
+ VERSION = "0.6.3"
6
6
  end
7
7
  end
data/renovate.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "extends": [
4
4
  "config:recommended",
5
5
  ":automergeStableNonMajor",
6
- "npm:unpublishSafe"
6
+ "security:minimumReleaseAgeNpm"
7
7
  ],
8
8
  "semanticCommits": "enabled"
9
9
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openfeature-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -159,6 +159,7 @@ files:
159
159
  - LICENSE
160
160
  - README.md
161
161
  - Rakefile
162
+ - cucumber.yml
162
163
  - lib/open_feature/sdk.rb
163
164
  - lib/open_feature/sdk/api.rb
164
165
  - lib/open_feature/sdk/client.rb