openfeature-sdk 0.8.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a637edf0b0a85dbfbaf655066912a64a21c1cd4a467b4fe557208c10184df3a9
4
- data.tar.gz: 6fa0f3f6e534279a8e1b6296e1e8b524decd315c99e4bc6238575f3867a8a428
3
+ metadata.gz: cf021dbf998f01e1b0f6e69723e65e32b7f5d36e91381963988af89daabcbfd9
4
+ data.tar.gz: bbe22d9c478d5e2608d888d8a627dcdf3477b99f31b9b5114fce071fa18fb669
5
5
  SHA512:
6
- metadata.gz: d16e851abe52d3bbbac68aa9180a9c1ace7b7cf97ce3508c21e083d7780825b862a608a7f438598d6bcb44c50efeceb84148c2f5351d544cb5584546d2cb2c22
7
- data.tar.gz: 18c44f236a822a02469a4ed66b297c607e7e59d440428266790ce1a95da21a549e122c605d7d1f9c70af9442d8f6ce8c0c7ff0242e1f69268ce242bb7dbb02b8
6
+ metadata.gz: 417abbc5e8c3e216e5bfd6299bad2f4f46396ed7e3e2a712c7e1a9c92639fa82cde9a904616854428ad54de10986e8537183b6a5efc959e3c7350943874e7a37
7
+ data.tar.gz: e715404890c82fd6b36b3d7c965a6c0e4eea06c80973ca25f3932aba943636ceb5addfe46ce06fdd59de4d5332e704e29dcb28f16ad7a690869b4da94a217524
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.8.0"
2
+ ".": "0.8.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.1](https://github.com/open-feature/ruby-sdk/compare/v0.8.0...v0.8.1) (2026-10-23)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * close the spec v0.9.0 gaps the compliance audit found ([#361](https://github.com/open-feature/ruby-sdk/issues/361)) ([1ecbb58](https://github.com/open-feature/ruby-sdk/commit/1ecbb58d52a26cdc6c2e187bdf61d037d7309b20))
9
+
3
10
  ## [0.8.0](https://github.com/open-feature/ruby-sdk/compare/v0.7.1...v0.8.0) (2026-10-23)
4
11
 
5
12
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.8.0)
4
+ openfeature-sdk (0.8.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -12,13 +12,13 @@
12
12
  <!-- x-hide-in-docs-end -->
13
13
  <!-- The 'github-badges' class is used in the docs -->
14
14
  <p align="center" class="github-badges">
15
- <a href="https://github.com/open-feature/spec/releases/tag/v0.8.0">
16
- <img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge" />
15
+ <a href="https://github.com/open-feature/spec/releases/tag/v0.9.0">
16
+ <img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.9.0&color=yellow&style=for-the-badge" />
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.8.0">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.8.0&color=blue&style=for-the-badge" />
20
+ <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.8.1">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.8.1&color=blue&style=for-the-badge" />
22
22
  </a>
23
23
 
24
24
  <!-- x-release-please-end -->
@@ -506,6 +506,12 @@ class MyProvider
506
506
  # Return value is ignored
507
507
  end
508
508
 
509
+ # Optional: hooks the SDK runs on every evaluation that uses this provider (spec 2.3.1).
510
+ # They run after API, client and invocation hooks in the before stage, and first in the after, error and finally stages.
511
+ def hooks
512
+ []
513
+ end
514
+
509
515
  def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
510
516
  # Retrieve a boolean value from provider source
511
517
  # To signal an error, return ResolutionDetails with an error_code, or raise
@@ -566,10 +572,11 @@ class MyLoggingHook
566
572
  end
567
573
  ```
568
574
 
569
- Hooks can read their inputs but not change them:
575
+ Hooks can read the hook context and hints but not change them:
570
576
 
571
- - Only `before` hooks can change the evaluation context: return an `EvaluationContext` for the SDK to merge in, or assign `hook_context.evaluation_context`. By the time `after`, `error` and `finally` run, it's a frozen copy, so assigning it or writing to its `fields` raises `FrozenError`. Nested field values aren't frozen, because they belong to the caller.
577
+ - Only `before` hooks can change the evaluation context: return an `EvaluationContext` for the SDK to merge in, or assign `hook_context.evaluation_context`. By the time `after`, `error` and `finally` run, it's a deep-frozen copy, so assigning it or changing its values raises `FrozenError`. Your own context isn't frozen.
572
578
  - The flag key, the default value, the hints, and the client and provider metadata are frozen copies. Your own objects aren't frozen.
579
+ - Only the spec's value types are copied: strings, numbers, booleans, times, hashes and arrays. Other objects, such as a user or a record you put in the context, are passed to hooks as they are. A time is copied but left unfrozen, so `utc` and `localtime` still work, and a hash's default proc isn't copied.
573
580
 
574
581
  > Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs!
575
582
 
@@ -30,7 +30,7 @@ module OpenFeature
30
30
  # If no provider is specified, the <tt>NoOpProvider</tt> is set as the default <tt>Provider</tt>.
31
31
  # Once the SDK has been configured, a client can be built
32
32
  #
33
- # client = OpenFeature::SDK::API.instance.build_client(name: 'my-open-feature-client')
33
+ # client = OpenFeature::SDK::API.instance.build_client(domain: 'my-open-feature-client')
34
34
  class API
35
35
  include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1; Isolated.create_api builds others (1.8.1)
36
36
  extend Forwardable
@@ -58,7 +58,8 @@ module OpenFeature
58
58
  built_context = build_evaluation_context(evaluation_context)
59
59
 
60
60
  provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
61
- rescue StandardError, NotImplementedError
61
+ nil # Spec 6.1.1.1: returns nothing
62
+ rescue *RECOVERABLE_ERRORS
62
63
  nil
63
64
  end
64
65
 
@@ -138,8 +139,8 @@ module OpenFeature
138
139
  evaluate_flag(provider: provider, type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
139
140
  end
140
141
  end
141
- rescue StandardError, NotImplementedError => e
142
- # Spec 1.4.10 and 1.4.11: report through the details, never raise or log. NotImplementedError is the abstract-method idiom.
142
+ rescue *RECOVERABLE_ERRORS => e
143
+ # Spec 1.4.10 and 1.4.11: report through the details, never raise or log
143
144
  resolution = Provider::ResolutionDetails.new(
144
145
  value: default_value,
145
146
  error_code: EvaluationError.code_for(e),
@@ -110,7 +110,7 @@ module OpenFeature
110
110
  @provider_state_registry.set_initial_state(prov, ProviderState::NOT_READY)
111
111
  prov.send(:detach) if prov.is_a?(Provider::EventEmitter)
112
112
  prov.shutdown if prov.respond_to?(:shutdown)
113
- rescue => e
113
+ rescue *RECOVERABLE_ERRORS => e
114
114
  @logger&.warn("Error shutting down provider #{prov&.class&.name || "unknown"}: #{e.message}")
115
115
  end
116
116
 
@@ -165,7 +165,8 @@ module OpenFeature
165
165
  # First, so a provider registered with another API (1.8.4) leaves no state behind
166
166
  provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
167
167
  attempt = @init_attempts[provider] = InitAttempt.new(:running, nil, Thread::Queue.new)
168
- @provider_state_registry.set_initial_state(provider)
168
+ # Spec 2.8.5.1: a provider without init is READY from registration
169
+ @provider_state_registry.set_initial_state(provider, has_init ? ProviderState::NOT_READY : ProviderState::READY)
169
170
  end
170
171
 
171
172
  # Publish last: clients read @providers without the lock, and must not see an untracked provider
@@ -184,30 +185,42 @@ module OpenFeature
184
185
  end
185
186
  end
186
187
 
187
- # Shutdown old provider outside mutex to avoid blocking other operations
188
- if needs_shutdown
189
- begin
190
- old_provider.shutdown if old_provider.respond_to?(:shutdown)
191
- rescue => e
192
- @logger&.warn("Error shutting down previous provider #{old_provider&.class&.name || "unknown"}: #{e.message}")
188
+ begin
189
+ # Spec 2.8.5.1: a provider without init is READY from registration, so its READY handlers run now, before the
190
+ # old provider's shutdown; after it, a handler added during the shutdown would run twice
191
+ unless has_init
192
+ if needs_init
193
+ init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: wait_for_init)
194
+ raise incomplete_init_error(provider, "initialization did not complete") if wait_for_init && attempt.state != :succeeded
195
+ elsif !old_provider.equal?(provider)
196
+ # Each new binding runs its domain's READY handlers
197
+ dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY, token: attempt)
198
+ end
199
+ end
200
+ ensure
201
+ # Shutdown old provider outside mutex to avoid blocking other operations. Checked again, since a READY
202
+ # handler above may have bound it again (a rollback)
203
+ if needs_shutdown && !provider_bound?(old_provider)
204
+ begin
205
+ old_provider.shutdown if old_provider.respond_to?(:shutdown)
206
+ rescue *RECOVERABLE_ERRORS => e
207
+ @logger&.warn("Error shutting down previous provider #{old_provider&.class&.name || "unknown"}: #{e.message}")
208
+ end
193
209
  end
194
210
  end
211
+ return unless has_init
195
212
 
196
213
  # Initialize provider outside the mutex to avoid blocking other operations
197
214
  if needs_init
198
- # Spec 2.8.5.1: a provider without init is READY, and its handlers have run, when set_provider returns
199
- if wait_for_init || !has_init
200
- init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: wait_for_init)
215
+ if wait_for_init
216
+ init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: true)
201
217
  # Replaced or shut down while init ran: fail like the waiters on this attempt did
202
- raise incomplete_init_error(provider, "initialization did not complete") if wait_for_init && attempt.state != :succeeded
218
+ raise incomplete_init_error(provider, "initialization did not complete") if attempt.state != :succeeded
203
219
  else
204
220
  init_thread = Thread.new do
205
221
  init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: false)
206
222
  end
207
223
  end
208
- elsif !has_init
209
- # Spec 2.8.5.1: each new binding of a provider without init runs its domain's READY handlers
210
- dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY, token: attempt) unless old_provider.equal?(provider)
211
224
  elsif wait_for_init
212
225
  # Already registered: wait for an init still in flight, then report its outcome.
213
226
  # From inside that init (e.g. a handler of an event it emits) the wait could never end.
@@ -232,7 +245,7 @@ module OpenFeature
232
245
  # An abandoned attempt (provider replaced or shut down meanwhile) reports no READY.
233
246
  # Spec 2.8.2/2.8.3: a provider that emits its own lifecycle events gets no synthetic READY or ERROR
234
247
  dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY, token: attempt) if finish_init_attempt(attempt, :succeeded) && !emits_lifecycle_events
235
- rescue => e
248
+ rescue *RECOVERABLE_ERRORS => e
236
249
  # Spec 1.7.5: PROVIDER_FATAL from init makes the provider FATAL
237
250
  error_code = EvaluationError.code_for(e)
238
251
  error = ProviderInitializationError.new(
@@ -250,7 +263,7 @@ module OpenFeature
250
263
 
251
264
  raise error if raise_on_error
252
265
  ensure
253
- # Reached unsettled only when init raised something other than a StandardError
266
+ # Reached unsettled only when init raised something not in RECOVERABLE_ERRORS, such as an Interrupt
254
267
  finish_init_attempt(attempt, :failed)
255
268
  end
256
269
 
@@ -296,7 +309,7 @@ module OpenFeature
296
309
  until queue.empty?
297
310
  begin
298
311
  deliver_provider_event(provider, *queue.shift)
299
- rescue => e
312
+ rescue *RECOVERABLE_ERRORS => e
300
313
  # Its own emitter already returned, so log rather than fail the event that was dispatching
301
314
  @logger&.error("Queued provider event failed: #{e.message}")
302
315
  end
@@ -405,7 +418,7 @@ module OpenFeature
405
418
 
406
419
  handlers.each do |handler|
407
420
  handler.call(event_details)
408
- rescue => e
421
+ rescue *RECOVERABLE_ERRORS => e
409
422
  @logger&.error("Client event handler failed: #{e.message}")
410
423
  end
411
424
  end
@@ -430,7 +443,7 @@ module OpenFeature
430
443
 
431
444
  begin
432
445
  handler.call(event_details)
433
- rescue => e
446
+ rescue *RECOVERABLE_ERRORS => e
434
447
  @logger&.error("Immediate API event handler failed: #{e.message}")
435
448
  end
436
449
  end
@@ -447,7 +460,7 @@ module OpenFeature
447
460
 
448
461
  begin
449
462
  handler.call(event_details)
450
- rescue => e
463
+ rescue *RECOVERABLE_ERRORS => e
451
464
  @logger&.error("Immediate client event handler failed: #{e.message}")
452
465
  end
453
466
  end
@@ -4,6 +4,10 @@ require_relative "provider/error_code"
4
4
 
5
5
  module OpenFeature
6
6
  module SDK
7
+ # What the SDK reports or logs, rather than raises, when provider, hook or handler code fails.
8
+ # NotImplementedError isn't a StandardError, but it's Ruby's abstract-method idiom; Interrupt and the rest still propagate.
9
+ RECOVERABLE_ERRORS = [StandardError, NotImplementedError].freeze
10
+
7
11
  # Raised by a provider to signal an evaluation error with an OpenFeature
8
12
  # error code (spec 2.2.7). Any exception responding to +error_code+ is
9
13
  # honored the same way.
@@ -55,7 +55,7 @@ module OpenFeature
55
55
  # Call handlers outside of mutex to avoid deadlocks
56
56
  handlers_to_call.each do |handler|
57
57
  handler.call(event_details)
58
- rescue => e
58
+ rescue *RECOVERABLE_ERRORS => e
59
59
  @logger&.warn "Event handler failed for #{event_type}: #{e.message}\n#{e.backtrace.join("\n")}"
60
60
  end
61
61
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module SDK
5
+ # Deep-frozen copies of the spec's value types (spec 3.1.2), so whoever gets one can't change it and the original is
6
+ # never frozen (spec 1.4.15.1, 4.1.3, 4.1.4.1, 4.2.2)
7
+ module FrozenCopy
8
+ def self.of(value, seen = nil)
9
+ return value if defined?(Ractor) && Ractor.shareable?(value)
10
+
11
+ case value
12
+ when String then value.frozen? ? value : value.dup.freeze
13
+ when Time then value.dup # Left unfrozen: utc and localtime convert in place, and on a copy they touch nothing else
14
+ when Hash, Array
15
+ seen ||= {}.compare_by_identity
16
+ return seen[value] if seen.key?(value)
17
+
18
+ # Registered before its items, so a cycle or a shared structure maps to this one copy
19
+ copy = seen[value] = value.dup
20
+ if copy.is_a?(Hash)
21
+ if copy.default_proc
22
+ copy.default_proc = nil # Behavior rather than data, and on a frozen copy one that stores would raise
23
+ elsif !copy.default.nil?
24
+ copy.default = of(copy.default, seen)
25
+ end
26
+ copy.transform_values! { |item| of(item, seen) }
27
+ else
28
+ copy.map! { |item| of(item, seen) }
29
+ end
30
+ copy.freeze
31
+ else
32
+ value # Anything else, like a user object or a record, is passed through as is
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "delegate"
4
- require_relative "frozen_copy"
4
+ require_relative "../frozen_copy"
5
5
 
6
6
  module OpenFeature
7
7
  module SDK
@@ -14,7 +14,7 @@ module OpenFeature
14
14
  assert_allowed_key(key)
15
15
  assert_allowed_value(value)
16
16
  end
17
- super(Hooks.frozen_copy(hash))
17
+ super(FrozenCopy.of(hash))
18
18
  freeze
19
19
  end
20
20
 
@@ -14,17 +14,17 @@ module OpenFeature
14
14
  nil
15
15
  end
16
16
 
17
- # Called after successful flag evaluation (spec 4.3.3).
17
+ # Called after successful flag evaluation (spec 4.3.6).
18
18
  def after(hook_context:, evaluation_details:, hints:)
19
19
  nil
20
20
  end
21
21
 
22
- # Called when an error occurs during flag evaluation (spec 4.3.6).
22
+ # Called when an error occurs during flag evaluation (spec 4.3.7).
23
23
  def error(hook_context:, exception:, hints:)
24
24
  nil
25
25
  end
26
26
 
27
- # Called unconditionally after flag evaluation (spec 4.3.7).
27
+ # Called unconditionally after flag evaluation (spec 4.3.8).
28
28
  def finally(hook_context:, evaluation_details:, hints:)
29
29
  nil
30
30
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "frozen_copy"
3
+ require_relative "../frozen_copy"
4
4
 
5
5
  module OpenFeature
6
6
  module SDK
@@ -13,33 +13,36 @@ module OpenFeature
13
13
  # - evaluation_context is mutable only in before hooks; HookExecutor freezes the hook context after them (4.1.4.1)
14
14
  class HookContext
15
15
  attr_reader :flag_key, :flag_value_type, :client_metadata, :provider_metadata
16
- attr_accessor :evaluation_context
16
+ attr_writer :evaluation_context
17
17
 
18
18
  def initialize(flag_key:, flag_value_type:, default_value:, evaluation_context:,
19
19
  client_metadata: nil, provider_metadata: nil)
20
- @flag_key = Hooks.frozen_copy(flag_key)
20
+ @flag_key = FrozenCopy.of(flag_key)
21
21
  @flag_value_type = flag_value_type.freeze
22
22
  @source_default_value = default_value
23
23
  @evaluation_context = evaluation_context
24
24
  @client_metadata = client_metadata
25
- @provider_metadata = Hooks.frozen_copy(provider_metadata)
25
+ @provider_metadata = frozen_metadata(provider_metadata)
26
26
  @hook_data = {}
27
27
  @copies = {}
28
28
  end
29
29
 
30
30
  # Copied on first read: most hooks never read it, and the copy grows with the value
31
31
  def default_value
32
- @copies.fetch(:default_value) { @copies[:default_value] = Hooks.frozen_copy(@source_default_value) }
32
+ @copies.fetch(:default_value) { @copies[:default_value] = FrozenCopy.of(@source_default_value) }
33
33
  end
34
34
 
35
- # Swaps in a frozen copy of the evaluation context, so neither the caller's nor a before hook's object is frozen.
36
- # Top-level strings are copied too, so a hook can't edit a targeting key in place.
37
- def freeze
38
- return self if frozen?
39
- @evaluation_context &&= @evaluation_context.dup.tap do |copy|
40
- copy.fields.transform_values! { |value| (value.is_a?(String) && !value.frozen?) ? value.dup.freeze : value }
41
- end.freeze
42
- super
35
+ # Live during the before stage; once frozen, a deep-frozen copy made on first read, so neither the caller's
36
+ # context nor one a before hook assigned is frozen
37
+ def evaluation_context
38
+ return @evaluation_context unless frozen?
39
+
40
+ @copies.fetch(:evaluation_context) do
41
+ @copies[:evaluation_context] = @evaluation_context&.dup&.tap do |copy|
42
+ seen = {}.compare_by_identity # One map for every field, so a structure two fields share is copied once
43
+ copy.fields.transform_values! { |value| FrozenCopy.of(value, seen) }
44
+ end&.freeze
45
+ end
43
46
  end
44
47
 
45
48
  # Returns a mutable hash scoped to the given hook instance.
@@ -48,6 +51,15 @@ module OpenFeature
48
51
  def hook_data_for(hook)
49
52
  @hook_data[hook.object_id] ||= {}
50
53
  end
54
+
55
+ private
56
+
57
+ # FrozenCopy passes Structs through, so ProviderMetadata's members are copied here (spec 4.2.2.3)
58
+ def frozen_metadata(metadata)
59
+ return metadata if !metadata.is_a?(Struct) || (defined?(Ractor) && Ractor.shareable?(metadata))
60
+
61
+ metadata.dup.tap { |copy| copy.each_pair { |member, value| copy[member] = FrozenCopy.of(value) } }.freeze
62
+ end
51
63
  end
52
64
  end
53
65
  end
@@ -50,7 +50,7 @@ module OpenFeature
50
50
  run_before_hooks(ordered_hooks, hook_context, hints)
51
51
  evaluation_details = evaluate_block.call(hook_context)
52
52
 
53
- # Spec 4.3.6: If evaluation resulted in an error (e.g. FLAG_NOT_FOUND,
53
+ # Spec 4.3.7: If evaluation resulted in an error (e.g. FLAG_NOT_FOUND,
54
54
  # TYPE_MISMATCH), run error hooks instead of after hooks.
55
55
  if evaluation_details.error_code
56
56
  error = EvaluationError.new(evaluation_details.error_message, error_code: evaluation_details.error_code)
@@ -58,7 +58,7 @@ module OpenFeature
58
58
  else
59
59
  run_after_hooks(ordered_hooks, hook_context, evaluation_details, hints)
60
60
  end
61
- rescue StandardError, NotImplementedError => e
61
+ rescue *RECOVERABLE_ERRORS => e
62
62
  run_error_hooks(ordered_hooks, hook_context, e, hints)
63
63
 
64
64
  evaluation_details = EvaluationDetails.new(
@@ -109,7 +109,7 @@ module OpenFeature
109
109
  hooks.reverse_each do |hook|
110
110
  next unless hook.respond_to?(:error)
111
111
  hook.error(hook_context: hook_context, exception: exception, hints: hints)
112
- rescue => e
112
+ rescue *RECOVERABLE_ERRORS => e
113
113
  @logger&.error("Error hook #{hook.class.name} failed: #{e.message}")
114
114
  end
115
115
  end
@@ -121,7 +121,7 @@ module OpenFeature
121
121
  hooks.reverse_each do |hook|
122
122
  next unless hook.respond_to?(:finally)
123
123
  hook.finally(hook_context: hook_context, evaluation_details: evaluation_details, hints: hints)
124
- rescue => e
124
+ rescue *RECOVERABLE_ERRORS => e
125
125
  @logger&.error("Finally hook #{hook.class.name} failed: #{e.message}")
126
126
  end
127
127
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../frozen_copy"
4
+
3
5
  module OpenFeature
4
6
  module SDK
5
7
  module Provider
@@ -7,14 +9,7 @@ module OpenFeature
7
9
 
8
10
  ResolutionDetails = Struct.new(:value, :reason, :variant, :error_code, :error_message, :flag_metadata, keyword_init: true) do
9
11
  def flag_metadata
10
- raw = self[:flag_metadata]
11
- if raw.nil?
12
- EMPTY_FLAG_METADATA
13
- elsif raw.frozen?
14
- raw
15
- else
16
- raw.dup.freeze
17
- end
12
+ FrozenCopy.of(self[:flag_metadata] || EMPTY_FLAG_METADATA)
18
13
  end
19
14
  end
20
15
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.8.0"
5
+ VERSION = "0.8.1"
6
6
  end
7
7
  end
@@ -1,5 +1,7 @@
1
1
  module OpenFeature
2
2
  module SDK
3
+ RECOVERABLE_ERRORS: Array[singleton(Exception)]
4
+
3
5
  class EvaluationError < StandardError
4
6
  attr_reader error_code: String
5
7
 
@@ -0,0 +1,7 @@
1
+ module OpenFeature
2
+ module SDK
3
+ module FrozenCopy
4
+ def self.of: (untyped value, ?Hash[untyped, untyped]? seen) -> untyped
5
+ end
6
+ end
7
+ end
@@ -6,15 +6,19 @@ module OpenFeature
6
6
  attr_reader flag_value_type: Symbol
7
7
  attr_reader client_metadata: ClientMetadata?
8
8
  attr_reader provider_metadata: Provider::ProviderMetadata?
9
- attr_accessor evaluation_context: EvaluationContext?
9
+ attr_writer evaluation_context: EvaluationContext?
10
10
 
11
11
  def initialize: (flag_key: String, flag_value_type: Symbol, default_value: untyped, evaluation_context: EvaluationContext?, ?client_metadata: ClientMetadata?, ?provider_metadata: Provider::ProviderMetadata?) -> void
12
12
 
13
13
  def default_value: () -> untyped
14
14
 
15
- def freeze: () -> self
15
+ def evaluation_context: () -> EvaluationContext?
16
16
 
17
17
  def hook_data_for: (untyped hook) -> Hash[untyped, untyped]
18
+
19
+ private
20
+
21
+ def frozen_metadata: (untyped metadata) -> untyped
18
22
  end
19
23
  end
20
24
  end
@@ -20,6 +20,10 @@ module OpenFeature
20
20
  def domain_scoped?: () -> bool
21
21
  end
22
22
 
23
+ interface _HookProvider
24
+ def hooks: () -> Array[Hooks::_Hook]
25
+ end
26
+
23
27
  interface _TrackableProvider
24
28
  def track: (String tracking_event_name, ?evaluation_context: EvaluationContext?, ?tracking_event_details: TrackingEventDetails?) -> void
25
29
  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.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -49,8 +49,8 @@ files:
49
49
  - lib/open_feature/sdk/evaluation_details.rb
50
50
  - lib/open_feature/sdk/evaluation_error.rb
51
51
  - lib/open_feature/sdk/event_dispatcher.rb
52
+ - lib/open_feature/sdk/frozen_copy.rb
52
53
  - lib/open_feature/sdk/hooks.rb
53
- - lib/open_feature/sdk/hooks/frozen_copy.rb
54
54
  - lib/open_feature/sdk/hooks/hints.rb
55
55
  - lib/open_feature/sdk/hooks/hook.rb
56
56
  - lib/open_feature/sdk/hooks/hook_context.rb
@@ -89,7 +89,7 @@ files:
89
89
  - sig/open_feature/sdk/evaluation_details.rbs
90
90
  - sig/open_feature/sdk/evaluation_error.rbs
91
91
  - sig/open_feature/sdk/event_dispatcher.rbs
92
- - sig/open_feature/sdk/hooks/frozen_copy.rbs
92
+ - sig/open_feature/sdk/frozen_copy.rbs
93
93
  - sig/open_feature/sdk/hooks/hints.rbs
94
94
  - sig/open_feature/sdk/hooks/hook.rbs
95
95
  - sig/open_feature/sdk/hooks/hook_context.rbs
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenFeature
4
- module SDK
5
- module Hooks
6
- # A deep-frozen copy of a hook input, so hooks can't change it and the caller's object isn't frozen (spec 4.1.3, 4.2.2)
7
- def self.frozen_copy(value)
8
- Ractor.make_shareable(value, copy: true)
9
- rescue
10
- value.dup.freeze # shortcut: when something inside can't be copied (a Mutex, Queue or Proc), only the top level is frozen
11
- end
12
- end
13
- end
14
- end
@@ -1,7 +0,0 @@
1
- module OpenFeature
2
- module SDK
3
- module Hooks
4
- def self.frozen_copy: [T] (T value) -> T
5
- end
6
- end
7
- end