Skip to content

Commit 28518a0

Browse files
josecolellaclaude
andauthored
feat: add hook data per-hook mutable state (#222)
Signed-off-by: Jose Colella <jose.colella@gusto.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a300fc5 commit 28518a0

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

lib/open_feature/sdk/hooks/hook_context.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ def initialize(flag_key:, flag_value_type:, default_value:, evaluation_context:,
2222
@evaluation_context = evaluation_context
2323
@client_metadata = client_metadata
2424
@provider_metadata = provider_metadata
25+
@hook_data = {}
26+
end
27+
28+
# Returns a mutable hash scoped to the given hook instance.
29+
# The same hash is returned across all hook stages (before, after, error, finally),
30+
# allowing hooks to share state across their lifecycle (spec 4.1.5, 4.6.1).
31+
def hook_data_for(hook)
32+
@hook_data[hook.object_id] ||= {}
2533
end
2634
end
2735
end

spec/specification/hooks_spec.rb

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,72 @@
8181
end
8282
end
8383

84+
context "Requirement 4.1.5" do
85+
specify "Hook context MUST provide a mechanism for hook instances to store and retrieve per-hook data" do
86+
data_across_stages = {}
87+
88+
hook = Class.new do
89+
include OpenFeature::SDK::Hooks::Hook
90+
91+
define_method(:before) do |hook_context:, hints:|
92+
hook_context.hook_data_for(self)["my_key"] = "set_in_before"
93+
nil
94+
end
95+
96+
define_method(:after) do |hook_context:, evaluation_details:, hints:|
97+
data_across_stages[:after_value] = hook_context.hook_data_for(self)["my_key"]
98+
end
99+
100+
define_method(:finally) do |hook_context:, evaluation_details:, hints:|
101+
data_across_stages[:finally_value] = hook_context.hook_data_for(self)["my_key"]
102+
end
103+
end.new
104+
105+
client = OpenFeature::SDK.build_client
106+
client.fetch_boolean_value(flag_key: "flag-1", default_value: false, hooks: [hook])
107+
108+
expect(data_across_stages[:after_value]).to eq("set_in_before")
109+
expect(data_across_stages[:finally_value]).to eq("set_in_before")
110+
end
111+
112+
specify "Hook data MUST be isolated between different hook instances" do
113+
hook_a_data = {}
114+
hook_b_data = {}
115+
116+
hook_a = Class.new do
117+
include OpenFeature::SDK::Hooks::Hook
118+
119+
define_method(:before) do |hook_context:, hints:|
120+
hook_context.hook_data_for(self)["owner"] = "hook_a"
121+
nil
122+
end
123+
124+
define_method(:after) do |hook_context:, evaluation_details:, hints:|
125+
hook_a_data[:owner] = hook_context.hook_data_for(self)["owner"]
126+
end
127+
end.new
128+
129+
hook_b = Class.new do
130+
include OpenFeature::SDK::Hooks::Hook
131+
132+
define_method(:before) do |hook_context:, hints:|
133+
hook_context.hook_data_for(self)["owner"] = "hook_b"
134+
nil
135+
end
136+
137+
define_method(:after) do |hook_context:, evaluation_details:, hints:|
138+
hook_b_data[:owner] = hook_context.hook_data_for(self)["owner"]
139+
end
140+
end.new
141+
142+
client = OpenFeature::SDK.build_client
143+
client.fetch_boolean_value(flag_key: "flag-1", default_value: false, hooks: [hook_a, hook_b])
144+
145+
expect(hook_a_data[:owner]).to eq("hook_a")
146+
expect(hook_b_data[:owner]).to eq("hook_b")
147+
end
148+
end
149+
84150
context "Requirement 4.1.4" do
85151
specify "evaluation context MUST be mutable" do
86152
captured_context = nil
@@ -246,6 +312,51 @@ def before(hook_context:, hints:)
246312
expect(finally_count).to eq(2)
247313
end
248314
end
315+
316+
context "Requirement 4.3.8" do
317+
specify "On success, finally hook receives evaluation details with the resolved value" do
318+
captured_details = nil
319+
320+
hook = Class.new do
321+
include OpenFeature::SDK::Hooks::Hook
322+
323+
define_method(:finally) do |hook_context:, evaluation_details:, hints:|
324+
captured_details = evaluation_details
325+
end
326+
end.new
327+
328+
client = OpenFeature::SDK.build_client
329+
client.fetch_boolean_value(flag_key: "flag-1", default_value: false, hooks: [hook])
330+
331+
expect(captured_details).not_to be_nil
332+
expect(captured_details.value).to eq(true)
333+
expect(captured_details.flag_key).to eq("flag-1")
334+
end
335+
336+
specify "On error, finally hook receives evaluation details with default value and error info" do
337+
captured_details = nil
338+
339+
hook = Class.new do
340+
include OpenFeature::SDK::Hooks::Hook
341+
342+
define_method(:finally) do |hook_context:, evaluation_details:, hints:|
343+
captured_details = evaluation_details
344+
end
345+
end.new
346+
347+
allow(provider).to receive(:fetch_boolean_value).and_raise("provider error")
348+
349+
client = OpenFeature::SDK.build_client
350+
client.fetch_boolean_value(flag_key: "flag-1", default_value: false, hooks: [hook])
351+
352+
expect(captured_details).not_to be_nil
353+
expect(captured_details.value).to eq(false)
354+
expect(captured_details.flag_key).to eq("flag-1")
355+
expect(captured_details.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::GENERAL)
356+
expect(captured_details.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
357+
expect(captured_details.error_message).to eq("provider error")
358+
end
359+
end
249360
end
250361

251362
context "4.4 - Hook Execution" do

0 commit comments

Comments
 (0)