diff --git a/lib/open_feature/sdk/hooks/hook_context.rb b/lib/open_feature/sdk/hooks/hook_context.rb index b8b2c290..13ffe01a 100644 --- a/lib/open_feature/sdk/hooks/hook_context.rb +++ b/lib/open_feature/sdk/hooks/hook_context.rb @@ -22,6 +22,14 @@ def initialize(flag_key:, flag_value_type:, default_value:, evaluation_context:, @evaluation_context = evaluation_context @client_metadata = client_metadata @provider_metadata = provider_metadata + @hook_data = {} + end + + # Returns a mutable hash scoped to the given hook instance. + # The same hash is returned across all hook stages (before, after, error, finally), + # allowing hooks to share state across their lifecycle (spec 4.1.5, 4.6.1). + def hook_data_for(hook) + @hook_data[hook.object_id] ||= {} end end end diff --git a/spec/specification/hooks_spec.rb b/spec/specification/hooks_spec.rb index 03621a41..beb37432 100644 --- a/spec/specification/hooks_spec.rb +++ b/spec/specification/hooks_spec.rb @@ -81,6 +81,72 @@ end end + context "Requirement 4.1.5" do + specify "Hook context MUST provide a mechanism for hook instances to store and retrieve per-hook data" do + data_across_stages = {} + + hook = Class.new do + include OpenFeature::SDK::Hooks::Hook + + define_method(:before) do |hook_context:, hints:| + hook_context.hook_data_for(self)["my_key"] = "set_in_before" + nil + end + + define_method(:after) do |hook_context:, evaluation_details:, hints:| + data_across_stages[:after_value] = hook_context.hook_data_for(self)["my_key"] + end + + define_method(:finally) do |hook_context:, evaluation_details:, hints:| + data_across_stages[:finally_value] = hook_context.hook_data_for(self)["my_key"] + end + end.new + + client = OpenFeature::SDK.build_client + client.fetch_boolean_value(flag_key: "flag-1", default_value: false, hooks: [hook]) + + expect(data_across_stages[:after_value]).to eq("set_in_before") + expect(data_across_stages[:finally_value]).to eq("set_in_before") + end + + specify "Hook data MUST be isolated between different hook instances" do + hook_a_data = {} + hook_b_data = {} + + hook_a = Class.new do + include OpenFeature::SDK::Hooks::Hook + + define_method(:before) do |hook_context:, hints:| + hook_context.hook_data_for(self)["owner"] = "hook_a" + nil + end + + define_method(:after) do |hook_context:, evaluation_details:, hints:| + hook_a_data[:owner] = hook_context.hook_data_for(self)["owner"] + end + end.new + + hook_b = Class.new do + include OpenFeature::SDK::Hooks::Hook + + define_method(:before) do |hook_context:, hints:| + hook_context.hook_data_for(self)["owner"] = "hook_b" + nil + end + + define_method(:after) do |hook_context:, evaluation_details:, hints:| + hook_b_data[:owner] = hook_context.hook_data_for(self)["owner"] + end + end.new + + client = OpenFeature::SDK.build_client + client.fetch_boolean_value(flag_key: "flag-1", default_value: false, hooks: [hook_a, hook_b]) + + expect(hook_a_data[:owner]).to eq("hook_a") + expect(hook_b_data[:owner]).to eq("hook_b") + end + end + context "Requirement 4.1.4" do specify "evaluation context MUST be mutable" do captured_context = nil @@ -246,6 +312,51 @@ def before(hook_context:, hints:) expect(finally_count).to eq(2) end end + + context "Requirement 4.3.8" do + specify "On success, finally hook receives evaluation details with the resolved value" do + captured_details = nil + + hook = Class.new do + include OpenFeature::SDK::Hooks::Hook + + define_method(:finally) do |hook_context:, evaluation_details:, hints:| + captured_details = evaluation_details + end + end.new + + client = OpenFeature::SDK.build_client + client.fetch_boolean_value(flag_key: "flag-1", default_value: false, hooks: [hook]) + + expect(captured_details).not_to be_nil + expect(captured_details.value).to eq(true) + expect(captured_details.flag_key).to eq("flag-1") + end + + specify "On error, finally hook receives evaluation details with default value and error info" do + captured_details = nil + + hook = Class.new do + include OpenFeature::SDK::Hooks::Hook + + define_method(:finally) do |hook_context:, evaluation_details:, hints:| + captured_details = evaluation_details + end + end.new + + allow(provider).to receive(:fetch_boolean_value).and_raise("provider error") + + client = OpenFeature::SDK.build_client + client.fetch_boolean_value(flag_key: "flag-1", default_value: false, hooks: [hook]) + + expect(captured_details).not_to be_nil + expect(captured_details.value).to eq(false) + expect(captured_details.flag_key).to eq("flag-1") + expect(captured_details.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::GENERAL) + expect(captured_details.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR) + expect(captured_details.error_message).to eq("provider error") + end + end end context "4.4 - Hook Execution" do