openfeature-sdk 0.7.1 → 0.8.0
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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +1 -1
- data/README.md +7 -2
- data/lib/open_feature/sdk/client.rb +11 -6
- data/lib/open_feature/sdk/evaluation_context.rb +10 -0
- data/lib/open_feature/sdk/hooks/frozen_copy.rb +14 -0
- data/lib/open_feature/sdk/hooks/hints.rb +2 -2
- data/lib/open_feature/sdk/hooks/hook_context.rb +25 -8
- data/lib/open_feature/sdk/hooks/hook_executor.rb +9 -2
- data/lib/open_feature/sdk/version.rb +1 -1
- data/sig/open_feature/sdk/evaluation_context.rbs +4 -0
- data/sig/open_feature/sdk/hooks/frozen_copy.rbs +7 -0
- data/sig/open_feature/sdk/hooks/hook_context.rbs +4 -1
- data/sig/open_feature/sdk/hooks/hook_executor.rbs +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a637edf0b0a85dbfbaf655066912a64a21c1cd4a467b4fe557208c10184df3a9
|
|
4
|
+
data.tar.gz: 6fa0f3f6e534279a8e1b6296e1e8b524decd315c99e4bc6238575f3867a8a428
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d16e851abe52d3bbbac68aa9180a9c1ace7b7cf97ce3508c21e083d7780825b862a608a7f438598d6bcb44c50efeceb84148c2f5351d544cb5584546d2cb2c22
|
|
7
|
+
data.tar.gz: 18c44f236a822a02469a4ed66b297c607e7e59d440428266790ce1a95da21a549e122c605d7d1f9c70af9442d8f6ce8c0c7ff0242e1f69268ce242bb7dbb02b8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0](https://github.com/open-feature/ruby-sdk/compare/v0.7.1...v0.8.0) (2026-10-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* make hook inputs immutable ([#357](https://github.com/open-feature/ruby-sdk/issues/357))
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* do not mutate the provider's ResolutionDetails ([#358](https://github.com/open-feature/ruby-sdk/issues/358)) ([ec657a5](https://github.com/open-feature/ruby-sdk/commit/ec657a5fc7b1514828ea2fa1cb6bfe017029f5af))
|
|
13
|
+
* make hook inputs immutable ([#357](https://github.com/open-feature/ruby-sdk/issues/357)) ([9e166fe](https://github.com/open-feature/ruby-sdk/commit/9e166fe09ec2a10d220295b600141cb351516b24))
|
|
14
|
+
|
|
3
15
|
## [0.7.1](https://github.com/open-feature/ruby-sdk/compare/v0.7.0...v0.7.1) (2026-10-23)
|
|
4
16
|
|
|
5
17
|
|
data/Gemfile.lock
CHANGED
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.
|
|
21
|
-
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.
|
|
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" />
|
|
22
22
|
</a>
|
|
23
23
|
|
|
24
24
|
<!-- x-release-please-end -->
|
|
@@ -566,6 +566,11 @@ class MyLoggingHook
|
|
|
566
566
|
end
|
|
567
567
|
```
|
|
568
568
|
|
|
569
|
+
Hooks can read their inputs but not change them:
|
|
570
|
+
|
|
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.
|
|
572
|
+
- The flag key, the default value, the hints, and the client and provider metadata are frozen copies. Your own objects aren't frozen.
|
|
573
|
+
|
|
569
574
|
> 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!
|
|
570
575
|
|
|
571
576
|
<!-- x-hide-in-docs-start -->
|
|
@@ -134,7 +134,7 @@ module OpenFeature
|
|
|
134
134
|
executor.run_short_circuit(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints, evaluation_details: evaluation_details)
|
|
135
135
|
evaluation_details
|
|
136
136
|
else
|
|
137
|
-
executor.execute(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints) do |hctx|
|
|
137
|
+
executor.execute(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints, default_value:) do |hctx|
|
|
138
138
|
evaluate_flag(provider: provider, type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
|
|
139
139
|
end
|
|
140
140
|
end
|
|
@@ -157,11 +157,16 @@ module OpenFeature
|
|
|
157
157
|
evaluation_context: evaluation_context
|
|
158
158
|
)
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
resolution_details
|
|
164
|
-
|
|
160
|
+
provider_error = resolution_details.error_code
|
|
161
|
+
if provider_error || TYPE_CLASS_MAP[type].none? { |klass| resolution_details.value.is_a?(klass) }
|
|
162
|
+
# A new struct, since a provider may cache and reuse the one it returned
|
|
163
|
+
resolution_details = Provider::ResolutionDetails.new(
|
|
164
|
+
value: default_value,
|
|
165
|
+
reason: Provider::Reason::ERROR,
|
|
166
|
+
error_code: provider_error || Provider::ErrorCode::TYPE_MISMATCH,
|
|
167
|
+
error_message: provider_error ? resolution_details.error_message : "Expected #{type}, got #{resolution_details.value.class}",
|
|
168
|
+
flag_metadata: resolution_details.flag_metadata
|
|
169
|
+
)
|
|
165
170
|
end
|
|
166
171
|
|
|
167
172
|
EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
|
|
@@ -0,0 +1,14 @@
|
|
|
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,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "delegate"
|
|
4
|
+
require_relative "frozen_copy"
|
|
4
5
|
|
|
5
6
|
module OpenFeature
|
|
6
7
|
module SDK
|
|
@@ -13,8 +14,7 @@ module OpenFeature
|
|
|
13
14
|
assert_allowed_key(key)
|
|
14
15
|
assert_allowed_value(value)
|
|
15
16
|
end
|
|
16
|
-
|
|
17
|
-
super(@hash)
|
|
17
|
+
super(Hooks.frozen_copy(hash))
|
|
18
18
|
freeze
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -1,28 +1,45 @@
|
|
|
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 Hooks
|
|
6
8
|
# Provides context to hook stages during flag evaluation.
|
|
7
9
|
#
|
|
8
|
-
# Per spec 4.1.1-4.1.5:
|
|
10
|
+
# Per spec 4.1.1-4.1.5 and 4.2.2:
|
|
9
11
|
# - flag_key, flag_value_type, default_value are immutable (4.1.3)
|
|
10
|
-
# - client_metadata, provider_metadata are optional (4.1.2)
|
|
11
|
-
# - evaluation_context is mutable
|
|
12
|
+
# - client_metadata, provider_metadata are optional (4.1.2) and immutable (4.2.2.2, 4.2.2.3)
|
|
13
|
+
# - evaluation_context is mutable only in before hooks; HookExecutor freezes the hook context after them (4.1.4.1)
|
|
12
14
|
class HookContext
|
|
13
|
-
attr_reader :flag_key, :flag_value_type, :
|
|
14
|
-
:client_metadata, :provider_metadata
|
|
15
|
+
attr_reader :flag_key, :flag_value_type, :client_metadata, :provider_metadata
|
|
15
16
|
attr_accessor :evaluation_context
|
|
16
17
|
|
|
17
18
|
def initialize(flag_key:, flag_value_type:, default_value:, evaluation_context:,
|
|
18
19
|
client_metadata: nil, provider_metadata: nil)
|
|
19
|
-
@flag_key = flag_key
|
|
20
|
+
@flag_key = Hooks.frozen_copy(flag_key)
|
|
20
21
|
@flag_value_type = flag_value_type.freeze
|
|
21
|
-
@
|
|
22
|
+
@source_default_value = default_value
|
|
22
23
|
@evaluation_context = evaluation_context
|
|
23
24
|
@client_metadata = client_metadata
|
|
24
|
-
@provider_metadata = provider_metadata
|
|
25
|
+
@provider_metadata = Hooks.frozen_copy(provider_metadata)
|
|
25
26
|
@hook_data = {}
|
|
27
|
+
@copies = {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Copied on first read: most hooks never read it, and the copy grows with the value
|
|
31
|
+
def default_value
|
|
32
|
+
@copies.fetch(:default_value) { @copies[:default_value] = Hooks.frozen_copy(@source_default_value) }
|
|
33
|
+
end
|
|
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
|
|
26
43
|
end
|
|
27
44
|
|
|
28
45
|
# Returns a mutable hash scoped to the given hook instance.
|
|
@@ -13,6 +13,9 @@ module OpenFeature
|
|
|
13
13
|
# - Before/after hook error → stop remaining hooks, run error hooks, return default
|
|
14
14
|
# - Error hook error → log, continue remaining error hooks
|
|
15
15
|
# - Finally hook error → log, continue remaining finally hooks
|
|
16
|
+
#
|
|
17
|
+
# The hook context is frozen before after, error and finally hooks run, so only before hooks
|
|
18
|
+
# can change the evaluation context (spec 4.1.4.1). Not earlier: some providers write to it.
|
|
16
19
|
class HookExecutor
|
|
17
20
|
def initialize(logger: nil)
|
|
18
21
|
@logger = logger
|
|
@@ -37,9 +40,10 @@ module OpenFeature
|
|
|
37
40
|
# @param ordered_hooks [Array] hooks in before-order (API, Client, Invocation, Provider)
|
|
38
41
|
# @param hook_context [HookContext] the hook context
|
|
39
42
|
# @param hints [Hints] hook hints
|
|
43
|
+
# @param default_value [Object] returned if evaluation raises; the caller's own object, not the hooks' frozen copy
|
|
40
44
|
# @param evaluate_block [Proc] the flag evaluation to wrap
|
|
41
45
|
# @return [EvaluationDetails] the evaluation result
|
|
42
|
-
def execute(ordered_hooks:, hook_context:, hints:, &evaluate_block)
|
|
46
|
+
def execute(ordered_hooks:, hook_context:, hints:, default_value:, &evaluate_block)
|
|
43
47
|
evaluation_details = nil
|
|
44
48
|
|
|
45
49
|
begin
|
|
@@ -60,7 +64,7 @@ module OpenFeature
|
|
|
60
64
|
evaluation_details = EvaluationDetails.new(
|
|
61
65
|
flag_key: hook_context.flag_key,
|
|
62
66
|
resolution_details: Provider::ResolutionDetails.new(
|
|
63
|
-
value:
|
|
67
|
+
value: default_value,
|
|
64
68
|
error_code: EvaluationError.code_for(e),
|
|
65
69
|
reason: Provider::Reason::ERROR,
|
|
66
70
|
error_message: e.message
|
|
@@ -91,6 +95,7 @@ module OpenFeature
|
|
|
91
95
|
|
|
92
96
|
# Spec 4.4.2: After hooks run in reverse order: Provider → Invocation → Client → API
|
|
93
97
|
def run_after_hooks(hooks, hook_context, evaluation_details, hints)
|
|
98
|
+
hook_context.freeze
|
|
94
99
|
hooks.reverse_each do |hook|
|
|
95
100
|
next unless hook.respond_to?(:after)
|
|
96
101
|
hook.after(hook_context: hook_context, evaluation_details: evaluation_details, hints: hints)
|
|
@@ -100,6 +105,7 @@ module OpenFeature
|
|
|
100
105
|
# Spec 4.4.4: Error hooks run in reverse order.
|
|
101
106
|
# If an error hook itself errors, log and continue remaining error hooks.
|
|
102
107
|
def run_error_hooks(hooks, hook_context, exception, hints)
|
|
108
|
+
hook_context.freeze
|
|
103
109
|
hooks.reverse_each do |hook|
|
|
104
110
|
next unless hook.respond_to?(:error)
|
|
105
111
|
hook.error(hook_context: hook_context, exception: exception, hints: hints)
|
|
@@ -111,6 +117,7 @@ module OpenFeature
|
|
|
111
117
|
# Spec 4.4.3: Finally hooks run in reverse order unconditionally.
|
|
112
118
|
# If a finally hook errors, log and continue remaining finally hooks.
|
|
113
119
|
def run_finally_hooks(hooks, hook_context, evaluation_details, hints)
|
|
120
|
+
hook_context.freeze
|
|
114
121
|
hooks.reverse_each do |hook|
|
|
115
122
|
next unless hook.respond_to?(:finally)
|
|
116
123
|
hook.finally(hook_context: hook_context, evaluation_details: evaluation_details, hints: hints)
|
|
@@ -4,13 +4,16 @@ module OpenFeature
|
|
|
4
4
|
class HookContext
|
|
5
5
|
attr_reader flag_key: String
|
|
6
6
|
attr_reader flag_value_type: Symbol
|
|
7
|
-
attr_reader default_value: untyped
|
|
8
7
|
attr_reader client_metadata: ClientMetadata?
|
|
9
8
|
attr_reader provider_metadata: Provider::ProviderMetadata?
|
|
10
9
|
attr_accessor evaluation_context: EvaluationContext?
|
|
11
10
|
|
|
12
11
|
def initialize: (flag_key: String, flag_value_type: Symbol, default_value: untyped, evaluation_context: EvaluationContext?, ?client_metadata: ClientMetadata?, ?provider_metadata: Provider::ProviderMetadata?) -> void
|
|
13
12
|
|
|
13
|
+
def default_value: () -> untyped
|
|
14
|
+
|
|
15
|
+
def freeze: () -> self
|
|
16
|
+
|
|
14
17
|
def hook_data_for: (untyped hook) -> Hash[untyped, untyped]
|
|
15
18
|
end
|
|
16
19
|
end
|
|
@@ -6,7 +6,7 @@ module OpenFeature
|
|
|
6
6
|
|
|
7
7
|
def run_short_circuit: (ordered_hooks: Array[untyped], hook_context: HookContext, hints: Hints, evaluation_details: EvaluationDetails) -> void
|
|
8
8
|
|
|
9
|
-
def execute: (ordered_hooks: Array[untyped], hook_context: HookContext, hints: Hints) { (HookContext) -> EvaluationDetails } -> EvaluationDetails?
|
|
9
|
+
def execute: (ordered_hooks: Array[untyped], hook_context: HookContext, hints: Hints, default_value: untyped) { (HookContext) -> EvaluationDetails } -> EvaluationDetails?
|
|
10
10
|
|
|
11
11
|
private
|
|
12
12
|
|
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.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OpenFeature Authors
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
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
|
+
- lib/open_feature/sdk/hooks/frozen_copy.rb
|
|
53
54
|
- lib/open_feature/sdk/hooks/hints.rb
|
|
54
55
|
- lib/open_feature/sdk/hooks/hook.rb
|
|
55
56
|
- lib/open_feature/sdk/hooks/hook_context.rb
|
|
@@ -88,6 +89,7 @@ files:
|
|
|
88
89
|
- sig/open_feature/sdk/evaluation_details.rbs
|
|
89
90
|
- sig/open_feature/sdk/evaluation_error.rbs
|
|
90
91
|
- sig/open_feature/sdk/event_dispatcher.rbs
|
|
92
|
+
- sig/open_feature/sdk/hooks/frozen_copy.rbs
|
|
91
93
|
- sig/open_feature/sdk/hooks/hints.rbs
|
|
92
94
|
- sig/open_feature/sdk/hooks/hook.rbs
|
|
93
95
|
- sig/open_feature/sdk/hooks/hook_context.rbs
|