openfeature-sdk 0.6.5 → 0.6.6

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: 70a393d81225d8b8b546f985dffee74d8b12451e1b9c18d348e6ac3d6a7781e6
4
- data.tar.gz: 960f7f7ba3e704965b1dcb1d38c800d162874a9abf423ce0dc09695e8e7b7372
3
+ metadata.gz: 500c09c5333475a75aeff77cdab714c1360f4c14a84c28f9f16fe3b0dc42cb8f
4
+ data.tar.gz: 4d760e25308fad1ea762e8547260f87dea3474a8861eadd25993cfd2dde55b04
5
5
  SHA512:
6
- metadata.gz: e166c0c2698e277129734166ec0e0ed33d38329e090fa2760f720d653df93037e599ce83ba14eea803c99bb3be5b7c301e2e522dc99cc47f6299ce41699be451
7
- data.tar.gz: f74b128a0a376b5f934940226d5a96906b21500373afbce98d52267580a69fab59cc5b0b043f90546bfbf7ed3393ae5033d510e80c50f5abc57a77f10d5a93c7
6
+ metadata.gz: 4bfda5295966ab245426ecaa0d2dbb440b7f273833193906b9fd91fd6ffeb41e5b49e652a9d05dbf6679c08cfc434c4733d34384dfc1d64be995194f3938cdf1
7
+ data.tar.gz: 2359651d7407cdd30e41e15e4d764efc47565e025b2046adbc4942da0c25d62396bd5e52991626d728b675b123aa52bcce733f8ebc71703112ebe77d6704e96e
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.6.5"
2
+ ".": "0.6.6"
3
3
  }
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.2
1
+ 4.0.7
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 4.0.2
1
+ ruby 4.0.7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.6](https://github.com/open-feature/ruby-sdk/compare/v0.6.5...v0.6.6) (2026-10-22)
4
+
5
+
6
+ ### Features
7
+
8
+ * add PROVIDER_RECONCILING and PROVIDER_CONTEXT_CHANGED events ([#338](https://github.com/open-feature/ruby-sdk/issues/338)) ([2b93aa4](https://github.com/open-feature/ruby-sdk/commit/2b93aa4daf7087a818d0b758c7978e7ad3c1c589))
9
+ * support variant flags in InMemoryProvider ([#340](https://github.com/open-feature/ruby-sdk/issues/340)) ([1e4b168](https://github.com/open-feature/ruby-sdk/commit/1e4b168373e39522c4884e34383730d48772b95f))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * carry provider error codes through hooks ([#339](https://github.com/open-feature/ruby-sdk/issues/339)) ([b9e586c](https://github.com/open-feature/ruby-sdk/commit/b9e586c518a4b8e85facbb90e123c347b0db33e5))
15
+
3
16
  ## [0.6.5](https://github.com/open-feature/ruby-sdk/compare/v0.6.4...v0.6.5) (2026-03-18)
4
17
 
5
18
 
data/CLAUDE.md CHANGED
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Overview
6
6
 
7
- This is the official OpenFeature SDK for Ruby — an implementation of the [OpenFeature specification](https://openfeature.dev) providing a vendor-agnostic API for feature flag evaluation. Published as the `openfeature-sdk` gem. Requires Ruby >= 3.1.
7
+ This is the official OpenFeature SDK for Ruby — an implementation of the [OpenFeature specification](https://openfeature.dev) providing a vendor-agnostic API for feature flag evaluation. Published as the `openfeature-sdk` gem. Requires Ruby >= 3.4.
8
8
 
9
9
  ## Commands
10
10
 
data/Gemfile CHANGED
@@ -12,13 +12,13 @@ group :development, :test do
12
12
  gem "rspec", "~> 3.13.0"
13
13
  gem "standard"
14
14
  gem "standard-performance"
15
- gem "simplecov", "~> 0.22.0"
16
- gem "simplecov-cobertura", "~> 3.0"
17
- gem "steep", "~> 1.9"
15
+ gem "simplecov", "~> 1.3.0"
16
+ gem "simplecov-cobertura", "~> 4.0"
17
+ gem "steep", "~> 2.0"
18
18
  gem "timecop", "~> 0.9.10"
19
19
  end
20
20
 
21
21
  group :test do
22
- gem "cucumber", "~> 10.0"
22
+ gem "cucumber", "~> 11.0"
23
23
  gem "logger"
24
24
  end
data/Gemfile.lock CHANGED
@@ -1,116 +1,92 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.6.5)
4
+ openfeature-sdk (0.6.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- activesupport (8.1.2)
10
- base64
11
- bigdecimal
12
- concurrent-ruby (~> 1.0, >= 1.3.1)
13
- connection_pool (>= 2.2.5)
14
- drb
15
- i18n (>= 1.6, < 2)
16
- json
17
- logger (>= 1.4.2)
18
- minitest (>= 5.1)
19
- securerandom (>= 0.3)
20
- tzinfo (~> 2.0, >= 2.0.5)
21
- uri (>= 0.13.1)
22
9
  ast (2.4.3)
23
10
  base64 (0.3.0)
24
- bigdecimal (4.0.1)
11
+ bigdecimal (4.1.3)
25
12
  builder (3.3.0)
26
- concurrent-ruby (1.3.6)
27
- connection_pool (3.0.2)
28
- csv (3.3.5)
29
- cucumber (10.2.0)
13
+ concurrent-ruby (1.3.8)
14
+ csv (3.3.6)
15
+ cucumber (11.1.1)
30
16
  base64 (~> 0.2)
31
17
  builder (~> 3.2)
32
18
  cucumber-ci-environment (> 9, < 12)
33
- cucumber-core (> 15, < 17)
19
+ cucumber-core (>= 16.2.0, < 17)
34
20
  cucumber-cucumber-expressions (> 17, < 20)
35
- cucumber-html-formatter (> 21, < 23)
21
+ cucumber-html-formatter (> 21, < 24)
36
22
  diff-lcs (~> 1.5)
37
23
  logger (~> 1.6)
38
24
  mini_mime (~> 1.1)
39
25
  multi_test (~> 1.1)
40
- sys-uname (~> 1.3)
26
+ sys-uname (~> 1.5)
41
27
  cucumber-ci-environment (11.0.0)
42
28
  cucumber-core (16.2.0)
43
29
  cucumber-gherkin (> 36, < 40)
44
30
  cucumber-messages (> 31, < 33)
45
31
  cucumber-tag-expressions (> 6, < 9)
46
- cucumber-cucumber-expressions (19.0.0)
32
+ cucumber-cucumber-expressions (19.0.1)
47
33
  bigdecimal
48
- cucumber-gherkin (39.0.0)
34
+ cucumber-gherkin (39.1.0)
49
35
  cucumber-messages (>= 31, < 33)
50
- cucumber-html-formatter (22.3.0)
36
+ cucumber-html-formatter (23.1.0)
51
37
  cucumber-messages (> 23, < 33)
52
- cucumber-messages (32.2.0)
38
+ cucumber-messages (32.3.1)
53
39
  cucumber-tag-expressions (8.1.0)
54
- date (3.5.1)
55
40
  debug (1.11.1)
56
41
  irb (~> 1.10)
57
42
  reline (>= 0.3.8)
58
43
  diff-lcs (1.6.2)
59
- docile (1.4.1)
60
- drb (2.2.3)
61
- erb (6.0.2)
62
- ffi (1.17.3)
63
- fileutils (1.8.0)
64
- i18n (1.14.8)
65
- concurrent-ruby (~> 1.0)
66
- io-console (0.8.2)
67
- irb (1.17.0)
44
+ erb (6.0.7)
45
+ ffi (1.17.4)
46
+ fileutils (1.8.1)
47
+ io-console (0.9.4)
48
+ irb (1.18.0)
68
49
  pp (>= 0.6.0)
69
50
  prism (>= 1.3.0)
70
51
  rdoc (>= 4.0.0)
71
52
  reline (>= 0.4.2)
72
- json (2.18.1)
73
- language_server-protocol (3.17.0.5)
53
+ json (3.1.0)
54
+ language_server-protocol (3.17.0.6)
74
55
  lint_roller (1.1.0)
75
- listen (3.10.0)
56
+ listen (3.10.1)
76
57
  logger
77
58
  rb-fsevent (~> 0.10, >= 0.10.3)
78
59
  rb-inotify (~> 0.9, >= 0.9.10)
79
60
  logger (1.7.0)
80
- markly (0.15.2)
61
+ markly (0.19.0)
81
62
  memoist3 (1.0.0)
82
63
  mini_mime (1.1.5)
83
- minitest (6.0.2)
84
- drb (~> 2.0)
85
- prism (~> 1.5)
86
64
  multi_test (1.1.0)
87
- mutex_m (0.3.0)
88
- parallel (1.27.0)
89
- parser (3.3.10.2)
65
+ parallel (2.3.0)
66
+ parser (3.3.12.0)
90
67
  ast (~> 2.4.1)
91
68
  racc
92
- pp (0.6.3)
69
+ pp (0.6.4)
93
70
  prettyprint
94
- prettyprint (0.2.0)
71
+ prettyprint (0.2.1)
95
72
  prism (1.9.0)
96
- psych (5.3.1)
97
- date
98
- stringio
99
73
  racc (1.8.1)
100
74
  rainbow (3.1.1)
101
- rake (13.3.1)
75
+ rake (13.4.2)
102
76
  rb-fsevent (0.11.2)
103
77
  rb-inotify (0.11.1)
104
78
  ffi (~> 1.0)
105
- rbs (3.10.3)
79
+ rbs (4.2.0)
106
80
  logger
81
+ prism (>= 1.6.0)
107
82
  tsort
108
- rdoc (7.2.0)
83
+ rdoc (8.1.0)
109
84
  erb
110
- psych (>= 4.0.0)
85
+ prism (>= 1.6.0)
86
+ rbs (>= 4.0.0)
111
87
  tsort
112
- regexp_parser (2.11.3)
113
- reline (0.6.3)
88
+ regexp_parser (2.13.1)
89
+ reline (0.7.0)
114
90
  io-console (~> 0.5)
115
91
  rexml (3.4.4)
116
92
  rspec (3.13.2)
@@ -126,18 +102,18 @@ GEM
126
102
  diff-lcs (>= 1.2.0, < 2.0)
127
103
  rspec-support (~> 3.13.0)
128
104
  rspec-support (3.13.7)
129
- rubocop (1.84.2)
130
- json (~> 2.3)
105
+ rubocop (1.91.0)
106
+ json (>= 2.3)
131
107
  language_server-protocol (~> 3.17.0.2)
132
108
  lint_roller (~> 1.1.0)
133
- parallel (~> 1.10)
109
+ parallel (>= 1.10)
134
110
  parser (>= 3.3.0.2)
135
111
  rainbow (>= 2.2.2, < 4.0)
136
112
  regexp_parser (>= 2.9.3, < 3.0)
137
113
  rubocop-ast (>= 1.49.0, < 2.0)
138
114
  ruby-progressbar (~> 1.7)
139
115
  unicode-display_width (>= 2.4.0, < 4.0)
140
- rubocop-ast (1.49.0)
116
+ rubocop-ast (1.50.0)
141
117
  parser (>= 3.3.7.2)
142
118
  prism (~> 1.7)
143
119
  rubocop-performance (1.26.1)
@@ -146,19 +122,14 @@ GEM
146
122
  rubocop-ast (>= 1.47.1, < 2.0)
147
123
  ruby-progressbar (1.13.0)
148
124
  securerandom (0.4.1)
149
- simplecov (0.22.0)
150
- docile (~> 1.1)
151
- simplecov-html (~> 0.11)
152
- simplecov_json_formatter (~> 0.1)
153
- simplecov-cobertura (3.1.0)
125
+ simplecov (1.3.2)
126
+ simplecov-cobertura (4.0.0)
154
127
  rexml
155
- simplecov (~> 0.19)
156
- simplecov-html (0.13.2)
157
- simplecov_json_formatter (0.1.4)
158
- standard (1.54.0)
128
+ simplecov (~> 1.0)
129
+ standard (1.57.0)
159
130
  language_server-protocol (~> 3.17.0.2)
160
131
  lint_roller (~> 1.0)
161
- rubocop (~> 1.84.0)
132
+ rubocop (~> 1.91.0)
162
133
  standard-custom (~> 1.0.0)
163
134
  standard-performance (~> 1.8)
164
135
  standard-custom (1.0.2)
@@ -167,8 +138,7 @@ GEM
167
138
  standard-performance (1.9.0)
168
139
  lint_roller (~> 1.1)
169
140
  rubocop-performance (~> 1.26.0)
170
- steep (1.10.0)
171
- activesupport (>= 5.1)
141
+ steep (2.1.0)
172
142
  concurrent-ruby (>= 1.1.10)
173
143
  csv (>= 3.0.9)
174
144
  fileutils (>= 1.1.0)
@@ -176,29 +146,31 @@ GEM
176
146
  language_server-protocol (>= 3.17.0.4, < 4.0)
177
147
  listen (~> 3.0)
178
148
  logger (>= 1.3.0)
179
- mutex_m (>= 0.3.0)
180
- parser (>= 3.1)
149
+ parser (>= 3.2)
150
+ prism (>= 0.25.0)
181
151
  rainbow (>= 2.2.2, < 4.0)
182
- rbs (~> 3.9)
152
+ rbs (~> 4.2)
183
153
  securerandom (>= 0.1)
184
154
  strscan (>= 1.0.0)
185
155
  terminal-table (>= 2, < 5)
186
156
  uri (>= 0.12.0)
187
- stringio (3.2.0)
188
- strscan (3.1.7)
189
- sys-uname (1.5.0)
157
+ strscan (3.1.8)
158
+ sys-uname (1.5.1)
159
+ ffi (~> 1.1)
160
+ memoist3 (~> 1.0.0)
161
+ sys-uname (1.5.1-universal-mingw32)
190
162
  ffi (~> 1.1)
191
163
  memoist3 (~> 1.0.0)
164
+ win32ole
192
165
  terminal-table (4.0.0)
193
166
  unicode-display_width (>= 1.1.1, < 4)
194
- timecop (0.9.10)
167
+ timecop (0.9.11)
195
168
  tsort (0.2.0)
196
- tzinfo (2.0.6)
197
- concurrent-ruby (~> 1.0)
198
- unicode-display_width (3.2.0)
199
- unicode-emoji (~> 4.1)
200
- unicode-emoji (4.2.0)
201
- uri (1.1.1)
169
+ unicode-display_width (3.3.0)
170
+ unicode-emoji (~> 4.3)
171
+ unicode-emoji (4.3.0)
172
+ uri (1.1.2)
173
+ win32ole (1.9.3)
202
174
 
203
175
  PLATFORMS
204
176
  arm64-darwin-21
@@ -214,19 +186,19 @@ PLATFORMS
214
186
  x86_64-linux
215
187
 
216
188
  DEPENDENCIES
217
- cucumber (~> 10.0)
189
+ cucumber (~> 11.0)
218
190
  debug
219
191
  logger
220
192
  markly
221
193
  openfeature-sdk!
222
194
  rake (~> 13.0)
223
195
  rspec (~> 3.13.0)
224
- simplecov (~> 0.22.0)
225
- simplecov-cobertura (~> 3.0)
196
+ simplecov (~> 1.3.0)
197
+ simplecov-cobertura (~> 4.0)
226
198
  standard
227
199
  standard-performance
228
- steep (~> 1.9)
200
+ steep (~> 2.0)
229
201
  timecop (~> 0.9.10)
230
202
 
231
203
  BUNDLED WITH
232
- 2.3.25
204
+ 4.0.12
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.5">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.5&color=blue&style=for-the-badge" />
20
+ <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.6.6">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.6&color=blue&style=for-the-badge" />
22
22
  </a>
23
23
 
24
24
  <!-- x-release-please-end -->
@@ -70,7 +70,6 @@ gem install openfeature-sdk
70
70
 
71
71
  ```ruby
72
72
  require 'open_feature/sdk'
73
- require 'json' # For JSON.dump
74
73
 
75
74
  # API Initialization and configuration
76
75
 
@@ -79,7 +78,13 @@ OpenFeature::SDK.configure do |config|
79
78
  config.set_provider(OpenFeature::SDK::Provider::InMemoryProvider.new(
80
79
  {
81
80
  "flag1" => true,
82
- "flag2" => 1
81
+ "flag2" => 1,
82
+ # Variant flags report a variant and reason, and can target on the evaluation context
83
+ "flag3" => OpenFeature::SDK::Provider::InMemoryProvider::Flag.new(
84
+ variants: {"on" => true, "off" => false},
85
+ default_variant: "off",
86
+ context_evaluator: ->(ctx) { "on" if ctx&.targeting_key == "beta-user" }
87
+ )
83
88
  }
84
89
  ))
85
90
  end
@@ -91,7 +96,7 @@ client = OpenFeature::SDK.build_client
91
96
  bool_value = client.fetch_boolean_value(flag_key: 'boolean_flag', default_value: false)
92
97
 
93
98
  # a details method is also available for more information about the flag evaluation
94
- # see `ResolutionDetails` for more info
99
+ # see `EvaluationDetails` for more info
95
100
  bool_details = client.fetch_boolean_details(flag_key: 'boolean_flag', default_value: false)
96
101
 
97
102
  # fetching string value feature flag
@@ -158,9 +163,6 @@ rescue OpenFeature::SDK::ProviderInitializationError => e
158
163
  puts "Original error: #{e.original_error}"
159
164
  end
160
165
 
161
- # With custom timeout (default is 30 seconds)
162
- OpenFeature::SDK.set_provider_and_wait(my_provider, timeout: 60)
163
-
164
166
  # Domain-specific provider
165
167
  OpenFeature::SDK.set_provider_and_wait(my_provider, domain: "feature-flags")
166
168
 
@@ -454,6 +456,8 @@ class MyProvider
454
456
 
455
457
  def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
456
458
  # Retrieve a boolean value from provider source
459
+ # To signal an error, return ResolutionDetails with an error_code, or raise
460
+ # OpenFeature::SDK::EvaluationError.new("message", error_code: OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
457
461
  end
458
462
 
459
463
  def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
@@ -7,6 +7,7 @@ require_relative "configuration"
7
7
  require_relative "evaluation_context"
8
8
  require_relative "evaluation_context_builder"
9
9
  require_relative "evaluation_details"
10
+ require_relative "evaluation_error"
10
11
  require_relative "client_metadata"
11
12
  require_relative "hooks"
12
13
  require_relative "client"
@@ -80,7 +80,7 @@ module OpenFeature
80
80
  provider_hooks = @provider.respond_to?(:hooks) ? Array(@provider.hooks) : []
81
81
  ordered_hooks = [*OpenFeature::SDK.hooks, *@hooks, *invocation_hooks, *provider_hooks]
82
82
 
83
- # Check for short-circuit conditions (spec 1.7.6 + 1.7.7)
83
+ # Short-circuit NOT_READY/FATAL providers (required by evaluation_v2.feature)
84
84
  short_circuit_code = nil
85
85
  if OpenFeature::SDK.configuration.provider_tracked?(@provider)
86
86
  short_circuit_code = short_circuit_error_code(provider_status)
@@ -102,7 +102,7 @@ module OpenFeature
102
102
  rescue => e
103
103
  resolution = Provider::ResolutionDetails.new(
104
104
  value: default_value,
105
- error_code: Provider::ErrorCode::GENERAL,
105
+ error_code: EvaluationError.code_for(e),
106
106
  reason: Provider::Reason::ERROR,
107
107
  error_message: e.message
108
108
  )
@@ -130,7 +130,6 @@ module OpenFeature
130
130
  executor = Hooks::HookExecutor.new(logger: OpenFeature::SDK.configuration.logger)
131
131
 
132
132
  if short_circuit_code
133
- # Spec 1.7.6 + 1.7.7: short-circuit must still run error hooks and finally hooks
134
133
  resolution = Provider::ResolutionDetails.new(
135
134
  value: default_value,
136
135
  error_code: short_circuit_code,
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "timeout"
4
3
  require_relative "api"
5
4
  require_relative "provider_initialization_error"
5
+ require_relative "evaluation_error"
6
6
  require_relative "event_dispatcher"
7
7
  require_relative "provider_event"
8
8
  require_relative "provider_state_registry"
@@ -89,11 +89,17 @@ module OpenFeature
89
89
  end
90
90
 
91
91
  def shutdown
92
- providers_to_shutdown = @provider_mutex.synchronize { @providers.values.uniq(&:object_id) }
92
+ # Unbind before shutting down so events that arrive mid-shutdown are dropped
93
+ providers_to_shutdown = @provider_mutex.synchronize do
94
+ bound = @providers.values.uniq(&:object_id)
95
+ @providers = {}
96
+ bound
97
+ end
93
98
 
94
99
  providers_to_shutdown.each do |prov|
95
- # Spec 1.7.9: Set provider state to NOT_READY before shutdown
100
+ # Spec 1.7.6: Set provider state to NOT_READY before shutdown
96
101
  @provider_state_registry.set_initial_state(prov, ProviderState::NOT_READY)
102
+ prov.send(:detach) if prov.is_a?(Provider::EventEmitter)
97
103
  prov.shutdown if prov.respond_to?(:shutdown)
98
104
  rescue => e
99
105
  @logger&.warn("Error shutting down provider #{prov&.class&.name || "unknown"}: #{e.message}")
@@ -145,9 +151,11 @@ module OpenFeature
145
151
 
146
152
  # Spec 1.1.2.3: Only shutdown old provider if it's no longer bound to any domain
147
153
  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
154
+ needs_shutdown = !provider_bound?(old_provider)
155
+ if needs_shutdown
156
+ @provider_state_registry.remove_provider(old_provider)
157
+ old_provider.send(:detach) if old_provider.is_a?(Provider::EventEmitter)
158
+ end
151
159
  end
152
160
  end
153
161
 
@@ -187,12 +195,8 @@ module OpenFeature
187
195
 
188
196
  dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
189
197
  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
198
+ # Spec 1.7.5: PROVIDER_FATAL from init makes the provider FATAL
199
+ error_code = EvaluationError.code_for(e)
196
200
 
197
201
  dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
198
202
  error_code: error_code,
@@ -209,7 +213,12 @@ module OpenFeature
209
213
  end
210
214
 
211
215
  def dispatch_provider_event(provider, event_type, details = {})
212
- @provider_state_registry.update_state_from_event(provider, event_type, details)
216
+ # Under the replacement lock, so a late event can't re-track a replaced or shut-down provider
217
+ @provider_mutex.synchronize do
218
+ return unless provider_bound?(provider)
219
+
220
+ @provider_state_registry.update_state_from_event(provider, event_type, details)
221
+ end
213
222
 
214
223
  provider_name = extract_provider_name(provider)
215
224
 
@@ -222,6 +231,10 @@ module OpenFeature
222
231
 
223
232
  private
224
233
 
234
+ def provider_bound?(provider)
235
+ @providers.each_value.any? { |p| p.equal?(provider) }
236
+ end
237
+
225
238
  def extract_provider_name(provider)
226
239
  provider.respond_to?(:metadata) ? provider.metadata.name : provider.class.name
227
240
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider/error_code"
4
+
5
+ module OpenFeature
6
+ module SDK
7
+ # Raised by a provider to signal an evaluation error with an OpenFeature
8
+ # error code (spec 2.2.7). Any exception responding to +error_code+ is
9
+ # honored the same way.
10
+ class EvaluationError < StandardError
11
+ # @return [String] the OpenFeature error code
12
+ attr_reader :error_code
13
+
14
+ def initialize(message = nil, error_code: Provider::ErrorCode::GENERAL)
15
+ super(message || error_code)
16
+ @error_code = error_code
17
+ end
18
+
19
+ # The error code carried by +exception+, or GENERAL when it has none.
20
+ # Rescues a failing reader so callers inside a rescue block never raise (spec 1.4.10).
21
+ def self.code_for(exception)
22
+ (exception.respond_to?(:error_code) && exception.error_code) || Provider::ErrorCode::GENERAL
23
+ rescue
24
+ Provider::ErrorCode::GENERAL
25
+ end
26
+ end
27
+ end
28
+ end
@@ -18,15 +18,15 @@ 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.
21
+ # Runs error hooks and finally hooks for a short-circuit evaluation.
22
+ # Before hooks and after hooks are NOT run.
23
23
  #
24
24
  # @param ordered_hooks [Array] hooks in before-order
25
25
  # @param hook_context [HookContext] the hook context
26
26
  # @param hints [Hints] hook hints
27
27
  # @param evaluation_details [EvaluationDetails] the short-circuit result
28
28
  def run_short_circuit(ordered_hooks:, hook_context:, hints:, evaluation_details:)
29
- error = StandardError.new(evaluation_details.error_code)
29
+ error = EvaluationError.new(evaluation_details.error_message, error_code: evaluation_details.error_code)
30
30
  run_error_hooks(ordered_hooks, hook_context, error, hints)
31
31
  ensure
32
32
  run_finally_hooks(ordered_hooks, hook_context, evaluation_details, hints)
@@ -49,7 +49,7 @@ module OpenFeature
49
49
  # Spec 4.3.6: If evaluation resulted in an error (e.g. FLAG_NOT_FOUND,
50
50
  # TYPE_MISMATCH), run error hooks instead of after hooks.
51
51
  if evaluation_details.error_code
52
- error = StandardError.new(evaluation_details.error_message || evaluation_details.error_code)
52
+ error = EvaluationError.new(evaluation_details.error_message, error_code: evaluation_details.error_code)
53
53
  run_error_hooks(ordered_hooks, hook_context, error, hints)
54
54
  else
55
55
  run_after_hooks(ordered_hooks, hook_context, evaluation_details, hints)
@@ -61,7 +61,7 @@ module OpenFeature
61
61
  flag_key: hook_context.flag_key,
62
62
  resolution_details: Provider::ResolutionDetails.new(
63
63
  value: hook_context.default_value,
64
- error_code: Provider::ErrorCode::GENERAL,
64
+ error_code: EvaluationError.code_for(e),
65
65
  reason: Provider::Reason::ERROR,
66
66
  error_message: e.message
67
67
  )
@@ -49,8 +49,7 @@ module OpenFeature
49
49
 
50
50
  def build_error_message(hook_context, exception)
51
51
  parts = base_parts("error", hook_context)
52
- error_code = exception.respond_to?(:error_code) ? exception.error_code : Provider::ErrorCode::GENERAL
53
- parts << "error_code=#{sanitize(error_code)}"
52
+ parts << "error_code=#{sanitize(EvaluationError.code_for(exception))}"
54
53
  parts << "error_message=#{sanitize(exception.message)}"
55
54
  parts << "evaluation_context=#{format_context(hook_context.evaluation_context)}" if @include_evaluation_context
56
55
  parts.join(" ")
@@ -8,6 +8,10 @@ module OpenFeature
8
8
 
9
9
  NAME = "In-memory Provider"
10
10
 
11
+ # A flag with named variants. +context_evaluator+ receives the evaluation
12
+ # context and returns a variant key; anything else falls back to +default_variant+.
13
+ Flag = Struct.new(:variants, :default_variant, :context_evaluator, :flag_metadata, :disabled, keyword_init: true)
14
+
11
15
  attr_reader :metadata
12
16
 
13
17
  def initialize(flags = {})
@@ -70,6 +74,8 @@ module OpenFeature
70
74
  return ResolutionDetails.new(value: default_value, error_code: ErrorCode::FLAG_NOT_FOUND, reason: Reason::ERROR)
71
75
  end
72
76
 
77
+ return resolve_variant_flag(raw_value, default_value, evaluation_context) if raw_value.is_a?(Flag)
78
+
73
79
  if raw_value.respond_to?(:call)
74
80
  value = raw_value.call(evaluation_context)
75
81
  ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
@@ -78,6 +84,24 @@ module OpenFeature
78
84
  end
79
85
  end
80
86
 
87
+ def resolve_variant_flag(flag, default_value, evaluation_context)
88
+ flag_metadata = flag.flag_metadata
89
+ return ResolutionDetails.new(value: default_value, reason: Reason::DISABLED, flag_metadata:) if flag.disabled
90
+
91
+ variants = flag.variants.to_h
92
+ unless variants.key?(flag.default_variant)
93
+ return ResolutionDetails.new(value: default_value, error_code: ErrorCode::PARSE_ERROR, reason: Reason::ERROR, flag_metadata:)
94
+ end
95
+
96
+ matched = flag.context_evaluator&.call(evaluation_context)
97
+ if variants.key?(matched)
98
+ return ResolutionDetails.new(value: variants[matched], variant: matched, reason: Reason::TARGETING_MATCH, flag_metadata:)
99
+ end
100
+
101
+ reason = flag.context_evaluator ? Reason::DEFAULT : Reason::STATIC
102
+ ResolutionDetails.new(value: variants[flag.default_variant], variant: flag.default_variant, reason:, flag_metadata:)
103
+ end
104
+
81
105
  def emit_provider_changed(flag_keys)
82
106
  return unless configuration_attached?
83
107
 
@@ -13,12 +13,16 @@ module OpenFeature
13
13
  PROVIDER_ERROR = "PROVIDER_ERROR"
14
14
  PROVIDER_CONFIGURATION_CHANGED = "PROVIDER_CONFIGURATION_CHANGED"
15
15
  PROVIDER_STALE = "PROVIDER_STALE"
16
+ PROVIDER_RECONCILING = "PROVIDER_RECONCILING"
17
+ PROVIDER_CONTEXT_CHANGED = "PROVIDER_CONTEXT_CHANGED"
16
18
 
17
19
  ALL_EVENTS = [
18
20
  PROVIDER_READY,
19
21
  PROVIDER_ERROR,
20
22
  PROVIDER_CONFIGURATION_CHANGED,
21
- PROVIDER_STALE
23
+ PROVIDER_STALE,
24
+ PROVIDER_RECONCILING,
25
+ PROVIDER_CONTEXT_CHANGED
22
26
  ].freeze
23
27
  end
24
28
  end
@@ -91,7 +91,7 @@ module OpenFeature
91
91
 
92
92
  def state_from_event(event_type, event_details = nil)
93
93
  case event_type
94
- when ProviderEvent::PROVIDER_READY
94
+ when ProviderEvent::PROVIDER_READY, ProviderEvent::PROVIDER_CONTEXT_CHANGED
95
95
  ProviderState::READY
96
96
  when ProviderEvent::PROVIDER_STALE
97
97
  ProviderState::STALE
@@ -100,7 +100,7 @@ module OpenFeature
100
100
  when ProviderEvent::PROVIDER_CONFIGURATION_CHANGED
101
101
  nil # No state change per OpenFeature spec Requirement 5.3.5
102
102
  else
103
- nil # No state change for unknown events - conservative default
103
+ nil # PROVIDER_RECONCILING (RECONCILING status is static-context only, spec 1.7.2.1) and unknown events
104
104
  end
105
105
  end
106
106
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.6.5"
5
+ VERSION = "0.6.6"
6
6
  end
7
7
  end
data/renovate.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
3
  "extends": [
4
- "config:recommended",
4
+ "github>open-feature/community-tooling",
5
5
  ":automergeStableNonMajor",
6
6
  "security:minimumReleaseAgeNpm"
7
7
  ],
@@ -34,6 +34,7 @@ module OpenFeature
34
34
  def set_provider_internal: (untyped provider, domain: String?, wait_for_init: bool) -> void
35
35
  def init_provider: (untyped provider, EvaluationContext? context, ?raise_on_error: bool) -> void
36
36
  def dispatch_provider_event: (untyped provider, String event_type, ?Hash[Symbol, untyped] details) -> void
37
+ def provider_bound?: (untyped provider) -> bool
37
38
  def extract_provider_name: (untyped provider) -> String
38
39
  def run_handlers_for_provider: (untyped provider, String event_type, Hash[Symbol, untyped] event_details) -> void
39
40
  def run_immediate_handler: (String event_type, _ToProc handler, Client? client) -> void
@@ -0,0 +1,11 @@
1
+ module OpenFeature
2
+ module SDK
3
+ class EvaluationError < StandardError
4
+ attr_reader error_code: String
5
+
6
+ def initialize: (?String? message, ?error_code: String) -> void
7
+
8
+ def self.code_for: (Exception exception) -> String
9
+ end
10
+ end
11
+ end
@@ -6,6 +6,16 @@ module OpenFeature
6
6
 
7
7
  NAME: String
8
8
 
9
+ class Flag < Struct[untyped]
10
+ attr_accessor variants: Hash[String, untyped]?
11
+ attr_accessor default_variant: String?
12
+ attr_accessor context_evaluator: (^(EvaluationContext?) -> String?)?
13
+ attr_accessor flag_metadata: Hash[String, untyped]?
14
+ attr_accessor disabled: bool?
15
+
16
+ def initialize: (?variants: Hash[String, untyped]?, ?default_variant: String?, ?context_evaluator: (^(EvaluationContext?) -> String?)?, ?flag_metadata: Hash[String, untyped]?, ?disabled: bool?) -> void
17
+ end
18
+
9
19
  attr_reader metadata: ProviderMetadata
10
20
 
11
21
  def initialize: (?Hash[String, untyped] flags) -> void
@@ -28,6 +38,7 @@ module OpenFeature
28
38
  attr_reader flags: Hash[String, untyped]
29
39
 
30
40
  def fetch_value: (flag_key: String, default_value: untyped, evaluation_context: EvaluationContext?) -> ResolutionDetails
41
+ def resolve_variant_flag: (Flag flag, untyped default_value, EvaluationContext? evaluation_context) -> ResolutionDetails
31
42
  def emit_provider_changed: (Array[String] flag_keys) -> void
32
43
  end
33
44
  end
@@ -5,6 +5,8 @@ module OpenFeature
5
5
  PROVIDER_ERROR: String
6
6
  PROVIDER_CONFIGURATION_CHANGED: String
7
7
  PROVIDER_STALE: String
8
+ PROVIDER_RECONCILING: String
9
+ PROVIDER_CONTEXT_CHANGED: String
8
10
 
9
11
  ALL_EVENTS: Array[String]
10
12
  end
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.5
4
+ version: 0.6.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -20,7 +20,6 @@ files:
20
20
  - ".release-please-manifest.json"
21
21
  - ".rspec"
22
22
  - ".ruby-version"
23
- - ".simplecov"
24
23
  - ".standard.yml"
25
24
  - ".tool-versions"
26
25
  - ".yardopts"
@@ -48,6 +47,7 @@ files:
48
47
  - lib/open_feature/sdk/evaluation_context.rb
49
48
  - lib/open_feature/sdk/evaluation_context_builder.rb
50
49
  - lib/open_feature/sdk/evaluation_details.rb
50
+ - lib/open_feature/sdk/evaluation_error.rb
51
51
  - lib/open_feature/sdk/event_dispatcher.rb
52
52
  - lib/open_feature/sdk/hooks.rb
53
53
  - lib/open_feature/sdk/hooks/hints.rb
@@ -85,6 +85,7 @@ files:
85
85
  - sig/open_feature/sdk/evaluation_context.rbs
86
86
  - sig/open_feature/sdk/evaluation_context_builder.rbs
87
87
  - sig/open_feature/sdk/evaluation_details.rbs
88
+ - sig/open_feature/sdk/evaluation_error.rbs
88
89
  - sig/open_feature/sdk/event_dispatcher.rbs
89
90
  - sig/open_feature/sdk/hooks/hints.rbs
90
91
  - sig/open_feature/sdk/hooks/hook.rbs
@@ -132,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
133
  - !ruby/object:Gem::Version
133
134
  version: '0'
134
135
  requirements: []
135
- rubygems_version: 4.0.6
136
+ rubygems_version: 4.0.20
136
137
  specification_version: 4
137
138
  summary: OpenFeature SDK for Ruby
138
139
  test_files: []
data/.simplecov DELETED
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "simplecov-cobertura"
4
-
5
- SimpleCov.start do
6
- add_filter "/spec/"
7
- minimum_coverage 90
8
- if ENV["CI"] == "true"
9
- formatter SimpleCov::Formatter::CoberturaFormatter
10
- else
11
- formatter SimpleCov::Formatter::HTMLFormatter
12
- end
13
- end