diff --git a/openfeature/BUILD b/openfeature/BUILD index 0cd195d..62e2868 100644 --- a/openfeature/BUILD +++ b/openfeature/BUILD @@ -4,6 +4,44 @@ package( default_visibility = ["//visibility:public"], ) +cc_library( + name = "general_flag_evaluation_details", + hdrs = ["general_flag_evaluation_details.h"], + include_prefix = "openfeature", + deps = [ + ":error_code", + ":flag_metadata", + ":reason", + ":value", + ], +) + +cc_library( + name = "general_hook_context", + hdrs = ["general_hook_context.h"], + include_prefix = "openfeature", + deps = [ + ":evaluation_context", + ":flag_type_value", + ":hook_data", + ":metadata", + ":value", + ], +) + +cc_library( + name = "general_hook", + hdrs = ["general_hook.h"], + include_prefix = "openfeature", + deps = [ + ":evaluation_context", + ":general_flag_evaluation_details", + ":general_hook_context", + ":hook_hints", + ":value", + ], +) + cc_library( name = "client", hdrs = ["client.h"], @@ -74,6 +112,21 @@ cc_library( ], ) +cc_library( + name = "flag_evaluation_details", + srcs = ["flag_evaluation_details.cpp"], + hdrs = ["flag_evaluation_details.h"], + include_prefix = "openfeature", + deps = [ + ":general_flag_evaluation_details", + ":error_code", + ":flag_metadata", + ":reason", + ":resolution_details", + ":value" + ], +) + cc_library( name = "flag_metadata", hdrs = ["flag_metadata.h"], @@ -102,6 +155,7 @@ cc_library( hdrs = ["hook_context.h"], include_prefix = "openfeature", deps = [ + "general_hook_context", ":evaluation_context", ":flag_metadata", ":flag_type_value", @@ -124,6 +178,23 @@ cc_library( include_prefix = "openfeature", ) +cc_library( + name = "hook", + srcs = ["hook.cpp"], + hdrs = ["hook.h"], + include_prefix = "openfeature", + deps = [ + ":general_flag_evaluation_details", + ":general_hook_context", + ":general_hook", + ":evaluation_context", + ":flag_evaluation_details", + ":hook_context", + ":hook_hints", + ":value", + ], +) + cc_library( name = "metadata", hdrs = ["metadata.h"], diff --git a/openfeature/flag_evaluation_details.cpp b/openfeature/flag_evaluation_details.cpp new file mode 100644 index 0000000..9380538 --- /dev/null +++ b/openfeature/flag_evaluation_details.cpp @@ -0,0 +1,72 @@ +#include "openfeature/flag_evaluation_details.h" + +#include +#include +#include + +#include "openfeature/error_code.h" +#include "openfeature/flag_metadata.h" +#include "openfeature/reason.h" +#include "openfeature/resolution_details.h" +#include "openfeature/value.h" + +namespace openfeature { + +template +FlagEvaluationDetails::FlagEvaluationDetails( + std::string flag_key, T value, Reason reason, + std::optional variant, const FlagMetadata& flag_metadata, + std::optional error_code, + std::optional error_message) + : ResolutionDetails(std::move(value), reason, std::move(variant), + flag_metadata, error_code, std::move(error_message)), + flag_key_(std::move(flag_key)) {} + +template +FlagEvaluationDetails::FlagEvaluationDetails( + std::string flag_key, const ResolutionDetails& resolution_details) + : ResolutionDetails(resolution_details), + flag_key_(std::move(flag_key)) {} + +template +std::string_view FlagEvaluationDetails::GetFlagKey() const { + return flag_key_; +} + +template +Reason FlagEvaluationDetails::GetReason() const { + return ResolutionDetails::GetReason(); +} + +template +std::optional FlagEvaluationDetails::GetVariant() const { + return ResolutionDetails::GetVariant(); +} + +template +std::optional FlagEvaluationDetails::GetErrorCode() const { + return ResolutionDetails::GetErrorCode(); +} + +template +std::optional FlagEvaluationDetails::GetErrorMessage() const { + return ResolutionDetails::GetErrorMessage(); +} + +template +const FlagMetadata& FlagEvaluationDetails::GetFlagMetadata() const { + return ResolutionDetails::GetFlagMetadata(); +} + +template +Value FlagEvaluationDetails::GetValueAsValue() const { + return Value(ResolutionDetails::GetValue()); +} + +template class FlagEvaluationDetails; +template class FlagEvaluationDetails; +template class FlagEvaluationDetails; +template class FlagEvaluationDetails; +template class FlagEvaluationDetails; + +} // namespace openfeature diff --git a/openfeature/flag_evaluation_details.h b/openfeature/flag_evaluation_details.h new file mode 100644 index 0000000..c9ef595 --- /dev/null +++ b/openfeature/flag_evaluation_details.h @@ -0,0 +1,58 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_FLAG_EVALUATION_DETAILS_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_FLAG_EVALUATION_DETAILS_H_ + +#include +#include +#include + +#include "openfeature/error_code.h" +#include "openfeature/flag_metadata.h" +#include "openfeature/general_flag_evaluation_details.h" +#include "openfeature/reason.h" +#include "openfeature/resolution_details.h" +#include "openfeature/value.h" + +namespace openfeature { + +template +class FlagEvaluationDetails : public ResolutionDetails, + public GeneralFlagEvaluationDetails { + public: + FlagEvaluationDetails( + std::string flag_key, T value, Reason reason, + std::optional variant, const FlagMetadata& flag_metadata, + std::optional error_code = std::nullopt, + std::optional error_message = std::nullopt); + + FlagEvaluationDetails(std::string flag_key, + const ResolutionDetails& resolution_details); + + FlagEvaluationDetails(const FlagEvaluationDetails&) = default; + FlagEvaluationDetails& operator=(const FlagEvaluationDetails&) = default; + FlagEvaluationDetails(FlagEvaluationDetails&&) noexcept = default; + FlagEvaluationDetails& operator=(FlagEvaluationDetails&&) noexcept = default; + ~FlagEvaluationDetails() override = default; + + std::string_view GetFlagKey() const override; + Reason GetReason() const override; + std::optional GetVariant() const override; + std::optional GetErrorCode() const override; + std::optional GetErrorMessage() const override; + const FlagMetadata& GetFlagMetadata() const override; + + Value GetValueAsValue() const override; + + private: + std::string flag_key_; +}; + +// Type aliases for common types. +using BoolFlagEvaluationDetails = FlagEvaluationDetails; +using StringFlagEvaluationDetails = FlagEvaluationDetails; +using IntFlagEvaluationDetails = FlagEvaluationDetails; +using DoubleFlagEvaluationDetails = FlagEvaluationDetails; +using ObjectFlagEvaluationDetails = FlagEvaluationDetails; + +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_FLAG_EVALUATION_DETAILS_H_ diff --git a/openfeature/general_flag_evaluation_details.h b/openfeature/general_flag_evaluation_details.h new file mode 100644 index 0000000..364f3ba --- /dev/null +++ b/openfeature/general_flag_evaluation_details.h @@ -0,0 +1,29 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_GENERAL_FLAG_EVALUATION_DETAILS_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_GENERAL_FLAG_EVALUATION_DETAILS_H_ + +#include +#include +#include + +#include "openfeature/error_code.h" +#include "openfeature/flag_metadata.h" +#include "openfeature/reason.h" +#include "openfeature/value.h" + +namespace openfeature { + +class GeneralFlagEvaluationDetails { + public: + virtual ~GeneralFlagEvaluationDetails() = default; + virtual std::string_view GetFlagKey() const = 0; + virtual Value GetValueAsValue() const = 0; + virtual Reason GetReason() const = 0; + virtual std::optional GetVariant() const = 0; + virtual const FlagMetadata& GetFlagMetadata() const = 0; + virtual std::optional GetErrorCode() const = 0; + virtual std::optional GetErrorMessage() const = 0; +}; + +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_GENERAL_FLAG_EVALUATION_DETAILS_H_ diff --git a/openfeature/general_hook.h b/openfeature/general_hook.h new file mode 100644 index 0000000..205b653 --- /dev/null +++ b/openfeature/general_hook.h @@ -0,0 +1,48 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_GENERAL_HOOK_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_GENERAL_HOOK_H_ + +#include +#include +#include + +#include "openfeature/evaluation_context.h" +#include "openfeature/general_flag_evaluation_details.h" +#include "openfeature/general_hook_context.h" +#include "openfeature/hook_hints.h" +#include "openfeature/value.h" + +namespace openfeature { + +// Non-templated general class for all hooks. This will allow storing different +// hook types (e.g., BoolHook, +// StringHook) inside evaluation options. +class GeneralHook { + public: + GeneralHook() = default; + GeneralHook(const GeneralHook&) = delete; + GeneralHook(GeneralHook&&) = delete; + GeneralHook& operator=(const GeneralHook&) = delete; + GeneralHook& operator=(GeneralHook&&) = delete; + virtual ~GeneralHook() = default; + + // 1. Before: Runs before flag evaluation occurs. + virtual std::optional Before(const GeneralHookContext& ctx, + const HookHints& hints) { + return std::nullopt; + } + // 2. After: Runs immediately after successful flag evaluation. + virtual void After(const GeneralHookContext& ctx, + const GeneralFlagEvaluationDetails& details, + const HookHints& hints) {} + // 3. Error: Runs if an error occurs during evaluation or in Before/After. + virtual void Error(const GeneralHookContext& ctx, const std::exception& error, + const HookHints& hints) {} + // 4. Finally: Runs after evaluation occurs, regardless of success or error. + virtual void Finally(const GeneralHookContext& ctx, + const GeneralFlagEvaluationDetails& details, + const HookHints& hints) {} +}; + +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_GENERAL_HOOK_H_ \ No newline at end of file diff --git a/openfeature/general_hook_context.h b/openfeature/general_hook_context.h new file mode 100644 index 0000000..c0a5901 --- /dev/null +++ b/openfeature/general_hook_context.h @@ -0,0 +1,30 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_GENERAL_HOOK_CONTEXT_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_GENERAL_HOOK_CONTEXT_H_ + +#include +#include + +#include "openfeature/evaluation_context.h" +#include "openfeature/flag_type_value.h" +#include "openfeature/hook_data.h" +#include "openfeature/metadata.h" +#include "openfeature/value.h" + +namespace openfeature { + +class GeneralHookContext { + public: + virtual ~GeneralHookContext() = default; + virtual const std::string& GetFlagKey() const = 0; + virtual FlagValueType GetType() const = 0; + virtual Value GetDefaultValueAsValue() const = 0; + virtual const EvaluationContext& GetEvaluationContext() const = 0; + virtual void SetEvaluationContext(EvaluationContext ctx) = 0; + virtual const Metadata& GetClientMetadata() const = 0; + virtual const Metadata& GetProviderMetadata() const = 0; + virtual std::shared_ptr GetHookData() const = 0; +}; + +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_GENERAL_HOOK_CONTEXT_H_ \ No newline at end of file diff --git a/openfeature/hook.cpp b/openfeature/hook.cpp new file mode 100644 index 0000000..21af598 --- /dev/null +++ b/openfeature/hook.cpp @@ -0,0 +1,12 @@ +#include "openfeature/hook.h" + +namespace openfeature { + +// Explicit template instantiations for common hook types. +template class Hook; +template class Hook; +template class Hook; +template class Hook; +template class Hook; + +} // namespace openfeature diff --git a/openfeature/hook.h b/openfeature/hook.h new file mode 100644 index 0000000..ba28b69 --- /dev/null +++ b/openfeature/hook.h @@ -0,0 +1,101 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_HOOK_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_HOOK_H_ + +#include +#include +#include +#include + +#include "openfeature/evaluation_context.h" +#include "openfeature/flag_evaluation_details.h" +#include "openfeature/general_flag_evaluation_details.h" +#include "openfeature/general_hook.h" +#include "openfeature/general_hook_context.h" +#include "openfeature/hook_context.h" +#include "openfeature/hook_hints.h" +#include "openfeature/value.h" + +namespace openfeature { + +// Hook allows application developers to add arbitrary behavior to the +// flag evaluation lifecycle. Hooks operate similarly to middleware in web +// frameworks. They are executed stack-wise with respect to flag resolution, +// prioritizing increasing specificity (API, Client, Invocation, Provider) +// first, and the order in which they were added second. +/// https://openfeature.dev/specification/sections/hooks +template +class Hook : public GeneralHook { + public: + Hook() = default; + Hook(const Hook&) = delete; + Hook& operator=(const Hook&) = delete; + Hook(Hook&&) = delete; + Hook& operator=(Hook&&) = delete; + ~Hook() override = default; + + // Runs before the flag evaluation occurs. + std::optional Before(const GeneralHookContext& ctx, + const HookHints& hints) final { + if (auto* typed_ctx = dynamic_cast*>(&ctx)) { + return Before(*typed_ctx, hints); + } + return std::nullopt; + } + + // Runs immediately after successful flag evaluation occurs. + void After(const GeneralHookContext& ctx, + const GeneralFlagEvaluationDetails& details, + const HookHints& hints) final { + auto* typed_ctx = dynamic_cast*>(&ctx); + auto* typed_details = + dynamic_cast*>(&details); + if (typed_ctx && typed_details) { + After(*typed_ctx, *typed_details, hints); + } + } + + // Runs if an error occurs during flag evaluation or in `Before`/`After` + // stages. + void Error(const GeneralHookContext& ctx, const std::exception& error, + const HookHints& hints) final { + if (auto* typed_ctx = dynamic_cast*>(&ctx)) { + Error(*typed_ctx, error, hints); + } + } + + // Runs after the flag evaluation occurs, regardless of whether it was + // successful or not. + void Finally(const GeneralHookContext& ctx, + const GeneralFlagEvaluationDetails& details, + const HookHints& hints) final { + auto* typed_ctx = dynamic_cast*>(&ctx); + auto* typed_details = + dynamic_cast*>(&details); + if (typed_ctx && typed_details) { + Finally(*typed_ctx, *typed_details, hints); + } + } + + // Typed virtual methods for type-specific subclasses to override: + virtual std::optional Before(const HookContext& ctx, + const HookHints& hints) { + return std::nullopt; + } + virtual void After(const HookContext& ctx, + const FlagEvaluationDetails& details, + const HookHints& hints) {} + virtual void Error(const HookContext& ctx, const std::exception& error, + const HookHints& hints) {} + virtual void Finally(const HookContext& ctx, + const FlagEvaluationDetails& details, + const HookHints& hints) {} +}; + +using BoolHook = Hook; +using StringHook = Hook; +using IntHook = Hook; +using DoubleHook = Hook; +using ObjectHook = Hook; +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_HOOK_H_ diff --git a/openfeature/hook_context.cpp b/openfeature/hook_context.cpp index cabe909..efe49fc 100644 --- a/openfeature/hook_context.cpp +++ b/openfeature/hook_context.cpp @@ -36,6 +36,11 @@ FlagValueType HookContext::GetType() const { return type_; } +template +Value HookContext::GetDefaultValueAsValue() const { + return Value(default_value_); +} + template const T& HookContext::GetDefaultValue() const { return default_value_; diff --git a/openfeature/hook_context.h b/openfeature/hook_context.h index 935dae9..9360246 100644 --- a/openfeature/hook_context.h +++ b/openfeature/hook_context.h @@ -7,6 +7,7 @@ #include "openfeature/evaluation_context.h" #include "openfeature/flag_metadata.h" #include "openfeature/flag_type_value.h" +#include "openfeature/general_hook_context.h" #include "openfeature/hook_data.h" #include "openfeature/metadata.h" #include "openfeature/value.h" @@ -14,20 +15,22 @@ namespace openfeature { template -class HookContext { +class HookContext : public GeneralHookContext { public: HookContext(std::string flag_key, FlagValueType type, T default_value, EvaluationContext ctx, Metadata client_metadata, Metadata provider_metadata, std::shared_ptr hook_data); - const std::string& GetFlagKey() const; - FlagValueType GetType() const; + const std::string& GetFlagKey() const override; + FlagValueType GetType() const override; + const EvaluationContext& GetEvaluationContext() const override; + Value GetDefaultValueAsValue() const override; + void SetEvaluationContext(EvaluationContext ctx) override; + const Metadata& GetClientMetadata() const override; + const Metadata& GetProviderMetadata() const override; + std::shared_ptr GetHookData() const override; + const T& GetDefaultValue() const; - const EvaluationContext& GetEvaluationContext() const; - void SetEvaluationContext(EvaluationContext ctx); - const Metadata& GetClientMetadata() const; - const Metadata& GetProviderMetadata() const; - std::shared_ptr GetHookData() const; private: std::string flag_key_; diff --git a/test/BUILD b/test/BUILD index d4a5b39..a8942c1 100644 --- a/test/BUILD +++ b/test/BUILD @@ -42,6 +42,15 @@ cc_test( ], ) +cc_test( + name = "flag_evaluation_details_test", + srcs = ["flag_evaluation_details_test.cpp"], + deps = [ + "//openfeature:flag_evaluation_details", + "@googletest//:gtest_main", + ], +) + cc_test( name = "noop_provider_test", srcs = ["noop_provider_test.cpp"], @@ -112,11 +121,24 @@ cc_test( srcs = ["hook_context_test.cpp"], deps = [ "//openfeature:evaluation_context", - "//openfeature:flag_metadata", - "//openfeature:flag_type_value", "//openfeature:hook_context", "//openfeature:hook_data", - "//openfeature:metadata", + "//openfeature:value", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "hook_test", + srcs = ["hook_test.cpp"], + deps = [ + "//openfeature:general_hook", + "//openfeature:evaluation_context", + "//openfeature:flag_evaluation_details", + "//openfeature:flag_metadata", + "//openfeature:hook", + "//openfeature:hook_context", + "//openfeature:hook_hints", "//openfeature:value", "@googletest//:gtest_main", ], diff --git a/test/flag_evaluation_details_test.cpp b/test/flag_evaluation_details_test.cpp new file mode 100644 index 0000000..645f5be --- /dev/null +++ b/test/flag_evaluation_details_test.cpp @@ -0,0 +1,224 @@ +#include "openfeature/flag_evaluation_details.h" + +#include + +#include +#include +#include + +#include "openfeature/error_code.h" +#include "openfeature/flag_metadata.h" +#include "openfeature/reason.h" +#include "openfeature/resolution_details.h" +#include "openfeature/value.h" + +namespace openfeature { + +TEST(FlagEvaluationDetailsTest, DirectConstructorAccessesFieldsForBoolean) { + const std::string expected_flag_key = "bool-flag"; + const bool expected_value = true; + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "on-variant"; + const FlagMetadata expected_flag_metadata{}; + const std::optional expected_error_code = ErrorCode::kParseError; + const std::optional expected_error_message = + "Failed to parse data"; + + BoolFlagEvaluationDetails details( + expected_flag_key, expected_value, expected_reason, expected_variant, + expected_flag_metadata, expected_error_code, expected_error_message); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), expected_error_code); + ASSERT_EQ(details.GetErrorMessage(), expected_error_message); + ASSERT_NO_THROW(details.GetFlagMetadata()); +} + +TEST(FlagEvaluationDetailsTest, ResolutionDetailsConstructorForBoolean) { + const std::string expected_flag_key = "bool-flag-wrapped"; + const bool expected_value = false; + const Reason expected_reason = Reason::kDefault; + const std::optional expected_variant = "off-variant"; + const FlagMetadata expected_flag_metadata{}; + const std::optional expected_error_code = std::nullopt; + const std::optional expected_error_message = std::nullopt; + + BoolResolutionDetails res_details( + expected_value, expected_reason, expected_variant, expected_flag_metadata, + expected_error_code, expected_error_message); + + BoolFlagEvaluationDetails details(expected_flag_key, res_details); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), expected_error_code); + ASSERT_EQ(details.GetErrorMessage(), expected_error_message); + ASSERT_NO_THROW(details.GetFlagMetadata()); +} + +TEST(FlagEvaluationDetailsTest, DirectConstructorAccessesFieldsForString) { + const std::string expected_flag_key = "string-flag"; + const std::string expected_value = "expected-string"; + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "on-variant"; + const FlagMetadata expected_flag_metadata{}; + const std::optional expected_error_code = ErrorCode::kTypeMismatch; + const std::optional expected_error_message = + "Type mismatch error"; + + StringFlagEvaluationDetails details( + expected_flag_key, expected_value, expected_reason, expected_variant, + expected_flag_metadata, expected_error_code, expected_error_message); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), expected_error_code); + ASSERT_EQ(details.GetErrorMessage(), expected_error_message); + ASSERT_NO_THROW(details.GetFlagMetadata()); +} + +TEST(FlagEvaluationDetailsTest, ResolutionDetailsConstructorForString) { + const std::string expected_flag_key = "string-flag-wrapped"; + const std::string expected_value = "wrapped-string"; + const Reason expected_reason = Reason::kStatic; + const std::optional expected_variant = std::nullopt; + const FlagMetadata expected_flag_metadata{}; + + StringResolutionDetails res_details(expected_value, expected_reason, + expected_variant, expected_flag_metadata); + + StringFlagEvaluationDetails details(expected_flag_key, res_details); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), std::nullopt); + ASSERT_EQ(details.GetErrorMessage(), std::nullopt); +} + +TEST(FlagEvaluationDetailsTest, DirectConstructorAccessesFieldsForInteger) { + const std::string expected_flag_key = "int-flag"; + const int64_t expected_value = 123456789LL; + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "v1"; + const FlagMetadata expected_flag_metadata{}; + + IntFlagEvaluationDetails details(expected_flag_key, expected_value, + expected_reason, expected_variant, + expected_flag_metadata); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), std::nullopt); + ASSERT_EQ(details.GetErrorMessage(), std::nullopt); +} + +TEST(FlagEvaluationDetailsTest, ResolutionDetailsConstructorForInteger) { + const std::string expected_flag_key = "int-flag-wrapped"; + const int64_t expected_value = 42; + const Reason expected_reason = Reason::kCached; + const std::optional expected_variant = "v2"; + const FlagMetadata expected_flag_metadata{}; + + IntResolutionDetails res_details(expected_value, expected_reason, + expected_variant, expected_flag_metadata); + + IntFlagEvaluationDetails details(expected_flag_key, res_details); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), std::nullopt); + ASSERT_EQ(details.GetErrorMessage(), std::nullopt); +} + +TEST(FlagEvaluationDetailsTest, DirectConstructorAccessesFieldsForDouble) { + const std::string expected_flag_key = "double-flag"; + const double expected_value = 123.456; + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "on-variant"; + const FlagMetadata expected_flag_metadata{}; + + DoubleFlagEvaluationDetails details(expected_flag_key, expected_value, + expected_reason, expected_variant, + expected_flag_metadata); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_DOUBLE_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), std::nullopt); + ASSERT_EQ(details.GetErrorMessage(), std::nullopt); +} + +TEST(FlagEvaluationDetailsTest, ResolutionDetailsConstructorForDouble) { + const std::string expected_flag_key = "double-flag-wrapped"; + const double expected_value = 3.14159265359; + const Reason expected_reason = Reason::kSplit; + const std::optional expected_variant = "pi-variant"; + const FlagMetadata expected_flag_metadata{}; + + DoubleResolutionDetails res_details(expected_value, expected_reason, + expected_variant, expected_flag_metadata); + + DoubleFlagEvaluationDetails details(expected_flag_key, res_details); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_DOUBLE_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); +} + +TEST(FlagEvaluationDetailsTest, DirectConstructorAccessesFieldsForObject) { + const std::string expected_flag_key = "object-flag"; + const Value expected_value = Value(std::map{ + {"key1", Value("value1")}, {"key2", Value(42)}}); + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "object-variant"; + const FlagMetadata expected_flag_metadata{}; + + ObjectFlagEvaluationDetails details(expected_flag_key, expected_value, + expected_reason, expected_variant, + expected_flag_metadata); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), std::nullopt); + ASSERT_EQ(details.GetErrorMessage(), std::nullopt); +} + +TEST(FlagEvaluationDetailsTest, ResolutionDetailsConstructorForObject) { + const std::string expected_flag_key = "object-flag-wrapped"; + const Value expected_value = Value(std::map{ + {"nested", Value(true)}, {"score", Value(99.5)}}); + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "complex-variant"; + const FlagMetadata expected_flag_metadata{}; + + ObjectResolutionDetails res_details(expected_value, expected_reason, + expected_variant, expected_flag_metadata); + + ObjectFlagEvaluationDetails details(expected_flag_key, res_details); + + EXPECT_EQ(details.GetFlagKey(), expected_flag_key); + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), std::nullopt); + ASSERT_EQ(details.GetErrorMessage(), std::nullopt); +} + +} // namespace openfeature diff --git a/test/hook_context_test.cpp b/test/hook_context_test.cpp index 7425433..174a21b 100644 --- a/test/hook_context_test.cpp +++ b/test/hook_context_test.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -46,6 +47,29 @@ TEST_F(HookContextTest, ConstructorAndAccessorsForBool) { EXPECT_EQ(hook_ctx.GetClientMetadata().name, "test-client"); EXPECT_EQ(hook_ctx.GetProviderMetadata().name, "test-provider"); EXPECT_EQ(hook_ctx.GetHookData(), hook_data_); + + Value default_val = hook_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsBool()); + EXPECT_EQ(default_val.AsBool().value(), kDefaultValue); + + // Upcast to general interface reference (as a General Hook receives): + GeneralHookContext& general_ctx = hook_ctx; + + EXPECT_EQ(general_ctx.GetFlagKey(), "bool-flag"); + EXPECT_EQ(general_ctx.GetType(), kType); + // EXPECT_EQ(general_ctx.GetDefaultValue(), kDefaultValue); + + ASSERT_TRUE(general_ctx.GetEvaluationContext().GetTargetingKey().has_value()); + EXPECT_EQ(general_ctx.GetEvaluationContext().GetTargetingKey().value(), + "initial-user"); + + EXPECT_EQ(general_ctx.GetClientMetadata().name, "test-client"); + EXPECT_EQ(general_ctx.GetProviderMetadata().name, "test-provider"); + EXPECT_EQ(general_ctx.GetHookData(), hook_data_); + + default_val = general_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsBool()); + EXPECT_EQ(default_val.AsBool().value(), kDefaultValue); } TEST_F(HookContextTest, ConstructorAndAccessorsForString) { @@ -66,6 +90,28 @@ TEST_F(HookContextTest, ConstructorAndAccessorsForString) { EXPECT_EQ(hook_ctx.GetClientMetadata().name, "test-client"); EXPECT_EQ(hook_ctx.GetProviderMetadata().name, "test-provider"); EXPECT_EQ(hook_ctx.GetHookData(), hook_data_); + + Value default_val = hook_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsString()); + EXPECT_EQ(default_val.AsString().value(), default_value); + + // Upcast to general interface reference (as a General Hook receives): + GeneralHookContext& general_ctx = hook_ctx; + + EXPECT_EQ(general_ctx.GetFlagKey(), "string-flag"); + EXPECT_EQ(general_ctx.GetType(), kType); + + ASSERT_TRUE(general_ctx.GetEvaluationContext().GetTargetingKey().has_value()); + EXPECT_EQ(general_ctx.GetEvaluationContext().GetTargetingKey().value(), + "initial-user"); + + EXPECT_EQ(general_ctx.GetClientMetadata().name, "test-client"); + EXPECT_EQ(general_ctx.GetProviderMetadata().name, "test-provider"); + EXPECT_EQ(general_ctx.GetHookData(), hook_data_); + + default_val = general_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsString()); + EXPECT_EQ(default_val.AsString().value(), default_value); } TEST_F(HookContextTest, ConstructorAndAccessorsForInteger) { @@ -86,6 +132,28 @@ TEST_F(HookContextTest, ConstructorAndAccessorsForInteger) { EXPECT_EQ(hook_ctx.GetClientMetadata().name, "test-client"); EXPECT_EQ(hook_ctx.GetProviderMetadata().name, "test-provider"); EXPECT_EQ(hook_ctx.GetHookData(), hook_data_); + + Value default_val = hook_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsNumber()); + EXPECT_EQ(default_val.AsInt().value(), kDefaultValue); + + // Upcast to general interface reference (as a General Hook receives): + GeneralHookContext& general_ctx = hook_ctx; + + EXPECT_EQ(general_ctx.GetFlagKey(), "int-flag"); + EXPECT_EQ(general_ctx.GetType(), kType); + + ASSERT_TRUE(general_ctx.GetEvaluationContext().GetTargetingKey().has_value()); + EXPECT_EQ(general_ctx.GetEvaluationContext().GetTargetingKey().value(), + "initial-user"); + + EXPECT_EQ(general_ctx.GetClientMetadata().name, "test-client"); + EXPECT_EQ(general_ctx.GetProviderMetadata().name, "test-provider"); + EXPECT_EQ(general_ctx.GetHookData(), hook_data_); + + default_val = general_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsNumber()); + EXPECT_EQ(default_val.AsInt().value(), kDefaultValue); } TEST_F(HookContextTest, ConstructorAndAccessorsForDouble) { @@ -106,19 +174,54 @@ TEST_F(HookContextTest, ConstructorAndAccessorsForDouble) { EXPECT_EQ(hook_ctx.GetClientMetadata().name, "test-client"); EXPECT_EQ(hook_ctx.GetProviderMetadata().name, "test-provider"); EXPECT_EQ(hook_ctx.GetHookData(), hook_data_); + + Value default_val = hook_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsNumber()); + EXPECT_DOUBLE_EQ(default_val.AsDouble().value(), kDefaultValue); + + // Upcast to general interface reference (as a General Hook receives): + GeneralHookContext& general_ctx = hook_ctx; + + EXPECT_EQ(general_ctx.GetFlagKey(), "double-flag"); + EXPECT_EQ(general_ctx.GetType(), kType); + + ASSERT_TRUE(general_ctx.GetEvaluationContext().GetTargetingKey().has_value()); + EXPECT_EQ(general_ctx.GetEvaluationContext().GetTargetingKey().value(), + "initial-user"); + + EXPECT_EQ(general_ctx.GetClientMetadata().name, "test-client"); + EXPECT_EQ(general_ctx.GetProviderMetadata().name, "test-provider"); + EXPECT_EQ(general_ctx.GetHookData(), hook_data_); + + default_val = general_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsNumber()); + EXPECT_DOUBLE_EQ(default_val.AsDouble().value(), kDefaultValue); } TEST_F(HookContextTest, ConstructorAndAccessorsForObject) { constexpr FlagValueType kType = FlagValueType::kObject; - Value default_value(std::string("json-or-structure")); + constexpr bool kFeatureEnabled = true; + constexpr int64_t kMaxItems = 100LL; + const std::string default_theme = "dark"; + + std::map object_map{ + {"feature_enabled", Value(kFeatureEnabled)}, + {"max_items", Value(kMaxItems)}, + {"theme", Value(std::string(default_theme))}}; + Value default_value(object_map); ObjectHookContext hook_ctx("object-flag", kType, default_value, initial_ctx_, client_metadata_, provider_metadata_, hook_data_); EXPECT_EQ(hook_ctx.GetFlagKey(), "object-flag"); EXPECT_EQ(hook_ctx.GetType(), kType); - ASSERT_TRUE(hook_ctx.GetDefaultValue().IsString()); - EXPECT_EQ(hook_ctx.GetDefaultValue().AsString().value(), "json-or-structure"); + ASSERT_TRUE(hook_ctx.GetDefaultValue().IsStructure()); + const auto* struct_map = hook_ctx.GetDefaultValue().AsStructure(); + ASSERT_NE(struct_map, nullptr); + EXPECT_EQ(struct_map->at("feature_enabled").AsBool().value(), + kFeatureEnabled); + EXPECT_EQ(struct_map->at("max_items").AsInt().value(), kMaxItems); + EXPECT_EQ(struct_map->at("theme").AsString().value(), default_theme); ASSERT_TRUE(hook_ctx.GetEvaluationContext().GetTargetingKey().has_value()); EXPECT_EQ(hook_ctx.GetEvaluationContext().GetTargetingKey().value(), @@ -127,6 +230,38 @@ TEST_F(HookContextTest, ConstructorAndAccessorsForObject) { EXPECT_EQ(hook_ctx.GetClientMetadata().name, "test-client"); EXPECT_EQ(hook_ctx.GetProviderMetadata().name, "test-provider"); EXPECT_EQ(hook_ctx.GetHookData(), hook_data_); + + Value default_val = hook_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsStructure()); + ASSERT_NE(default_val.AsStructure(), nullptr); + EXPECT_EQ(default_val.AsStructure()->at("feature_enabled").AsBool().value(), + kFeatureEnabled); + EXPECT_EQ(default_val.AsStructure()->at("max_items").AsInt().value(), + kMaxItems); + EXPECT_EQ(default_val.AsStructure()->at("theme").AsString().value(), + default_theme); + + // Upcast to general interface reference (as a General Hook receives): + GeneralHookContext& general_ctx = hook_ctx; + + EXPECT_EQ(general_ctx.GetFlagKey(), "object-flag"); + EXPECT_EQ(general_ctx.GetType(), kType); + + ASSERT_TRUE(general_ctx.GetEvaluationContext().GetTargetingKey().has_value()); + EXPECT_EQ(general_ctx.GetEvaluationContext().GetTargetingKey().value(), + "initial-user"); + + EXPECT_EQ(general_ctx.GetClientMetadata().name, "test-client"); + EXPECT_EQ(general_ctx.GetProviderMetadata().name, "test-provider"); + EXPECT_EQ(general_ctx.GetHookData(), hook_data_); + + default_val = general_ctx.GetDefaultValueAsValue(); + ASSERT_TRUE(default_val.IsStructure()); + ASSERT_NE(default_val.AsStructure(), nullptr); + EXPECT_EQ(default_val.AsStructure()->at("feature_enabled").AsBool().value(), + true); + EXPECT_EQ(default_val.AsStructure()->at("max_items").AsInt().value(), 100LL); + EXPECT_EQ(default_val.AsStructure()->at("theme").AsString().value(), "dark"); } TEST_F(HookContextTest, SetEvaluationContextUpdatesContext) { diff --git a/test/hook_test.cpp b/test/hook_test.cpp new file mode 100644 index 0000000..418b7a5 --- /dev/null +++ b/test/hook_test.cpp @@ -0,0 +1,330 @@ +#include "openfeature/hook.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "openfeature/evaluation_context.h" +#include "openfeature/flag_metadata.h" +#include "openfeature/general_flag_evaluation_details.h" +#include "openfeature/general_hook.h" +#include "openfeature/hook_context.h" +#include "openfeature/hook_hints.h" +#include "openfeature/value.h" + +namespace openfeature { +namespace { + +template +class TrackingHook : public Hook { + public: + TrackingHook() = default; + + std::optional Before(const HookContext& ctx, + const HookHints& hints) override { + before_called = true; + last_flag_key = ctx.GetFlagKey(); + if (auto it_hints = hints.find("before_hint"); it_hints != hints.end()) { + last_hint_value = std::any_cast(it_hints->second); + } + if (return_context.has_value()) { + return return_context; + } + return std::nullopt; + } + + void After(const HookContext& ctx, const FlagEvaluationDetails& details, + const HookHints& hints) override { + after_called = true; + last_flag_key = ctx.GetFlagKey(); + last_reason = details.GetReason(); + if (auto it_hints = hints.find("after_hint"); it_hints != hints.end()) { + last_hint_value = std::any_cast(it_hints->second); + } + } + + void Error(const HookContext& ctx, const std::exception& error, + const HookHints& hints) override { + error_called = true; + last_flag_key = ctx.GetFlagKey(); + last_error_message = error.what(); + if (auto it_hints = hints.find("error_hint"); it_hints != hints.end()) { + last_hint_value = std::any_cast(it_hints->second); + } + } + + void Finally(const HookContext& ctx, + const FlagEvaluationDetails& details, + const HookHints& hints) override { + finally_called = true; + last_flag_key = ctx.GetFlagKey(); + last_reason = details.GetReason(); + if (auto it_hints = hints.find("finally_hint"); it_hints != hints.end()) { + last_hint_value = std::any_cast(it_hints->second); + } + } + + void SetReturnContext(std::optional ctx) { + return_context = std::move(ctx); + } + + bool before_called = false; + bool after_called = false; + bool error_called = false; + bool finally_called = false; + std::string last_flag_key; + std::string last_hint_value; + std::string last_error_message; + Reason last_reason = Reason::kUnknown; + std::optional return_context; +}; + +class GeneralTrackingHook : public GeneralHook { + public: + std::optional Before(const GeneralHookContext& ctx, + const HookHints& hints) override { + before_called = true; + last_flag_key = ctx.GetFlagKey(); + last_default_value = ctx.GetDefaultValueAsValue(); + return std::nullopt; + } + + void After(const GeneralHookContext& ctx, + const GeneralFlagEvaluationDetails& details, + const HookHints& hints) override { + after_called = true; + last_evaluated_value = details.GetValueAsValue(); + } + + bool before_called = false; + bool after_called = false; + std::string last_flag_key; + Value last_default_value; + Value last_evaluated_value; +}; + +class HookTest : public ::testing::Test { + protected: + HookTest() + : initial_ctx_( + EvaluationContext::Builder().WithTargetingKey("user-123").build()), + hook_data_(std::make_shared()) {} + + EvaluationContext initial_ctx_; + Metadata client_metadata_{"client-id"}; + Metadata provider_metadata_{"provider-id"}; + std::shared_ptr hook_data_; +}; + +} // namespace + +TEST_F(HookTest, GeneralHookReceivesAnyFlagType) { + GeneralTrackingHook general_hook; + BoolHookContext bool_ctx("bool-flag", FlagValueType::kBoolean, true, + initial_ctx_, client_metadata_, provider_metadata_, + hook_data_); + BoolFlagEvaluationDetails details("bool-flag", true, Reason::kStatic, + std::nullopt, FlagMetadata()); + HookHints hints; + + general_hook.Before(bool_ctx, hints); + general_hook.After(bool_ctx, details, hints); + + EXPECT_TRUE(general_hook.before_called); + EXPECT_TRUE(general_hook.after_called); + EXPECT_EQ(general_hook.last_flag_key, "bool-flag"); + EXPECT_EQ(general_hook.last_default_value.AsBool().value(), true); + EXPECT_EQ(general_hook.last_evaluated_value.AsBool().value(), true); +} + +TEST_F(HookTest, DefaultBeforeReturnsNulloptForAllSpecializations) { + constexpr bool kBoolValue = true; + constexpr int kIntValue = 100; + constexpr double kDoubleValue = 3.14; + const std::string string_value = "val"; + const Value object_value = Value("obj"); + HookHints hints; + + BoolHook bool_hook; + BoolHookContext bool_ctx("bool-flag", FlagValueType::kBoolean, kBoolValue, + initial_ctx_, client_metadata_, provider_metadata_, + hook_data_); + EXPECT_FALSE(bool_hook.Before(bool_ctx, hints).has_value()); + + StringHook string_hook; + StringHookContext string_ctx("string-flag", FlagValueType::kString, + string_value, initial_ctx_, client_metadata_, + provider_metadata_, hook_data_); + EXPECT_FALSE(string_hook.Before(string_ctx, hints).has_value()); + + IntHook int_hook; + IntHookContext int_ctx("int-flag", FlagValueType::kInteger, kIntValue, + initial_ctx_, client_metadata_, provider_metadata_, + hook_data_); + EXPECT_FALSE(int_hook.Before(int_ctx, hints).has_value()); + + DoubleHook double_hook; + DoubleHookContext double_ctx("double-flag", FlagValueType::kDouble, + kDoubleValue, initial_ctx_, client_metadata_, + provider_metadata_, hook_data_); + EXPECT_FALSE(double_hook.Before(double_ctx, hints).has_value()); + + ObjectHook object_hook; + ObjectHookContext object_ctx("object-flag", FlagValueType::kObject, + object_value, initial_ctx_, client_metadata_, + provider_metadata_, hook_data_); + EXPECT_FALSE(object_hook.Before(object_ctx, hints).has_value()); +} + +TEST_F(HookTest, DefaultAfterErrorAndFinallyAreNoOpsWithoutThrowing) { + constexpr bool kBoolValue = true; + BoolHook hook; + + BoolHookContext ctx("bool-flag", FlagValueType::kBoolean, kBoolValue, + initial_ctx_, client_metadata_, provider_metadata_, + hook_data_); + BoolFlagEvaluationDetails details("bool-flag", kBoolValue, Reason::kStatic, + std::nullopt, FlagMetadata()); + HookHints hints; + std::runtime_error error("simulated error"); + + EXPECT_NO_THROW(hook.After(ctx, details, hints)); + EXPECT_NO_THROW(hook.Error(ctx, error, hints)); + EXPECT_NO_THROW(hook.Finally(ctx, details, hints)); +} + +TEST_F(HookTest, OverriddenBeforeCanModifyAndReturnEvaluationContext) { + constexpr bool kBoolValue = true; + TrackingHook hook; + BoolHookContext ctx("bool-flag", FlagValueType::kBoolean, kBoolValue, + initial_ctx_, client_metadata_, provider_metadata_, + hook_data_); + HookHints hints{{"before_hint", std::any(std::string("hint-val"))}}; + + EvaluationContext modified_ctx = + EvaluationContext::Builder() + .WithTargetingKey("mutated-user") + .WithAttribute("region", std::string("us-east")) + .build(); + hook.SetReturnContext(modified_ctx); + + std::optional result = hook.Before(ctx, hints); + + EXPECT_TRUE(hook.before_called); + EXPECT_EQ(hook.last_flag_key, "bool-flag"); + EXPECT_EQ(hook.last_hint_value, "hint-val"); + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(result->GetTargetingKey().has_value()); + EXPECT_EQ(result->GetTargetingKey().value(), "mutated-user"); +} + +TEST_F(HookTest, OverriddenAfterReceivesContextDetailsAndHints) { + const std::string string_value = "default"; + TrackingHook hook; + StringHookContext ctx("string-flag", FlagValueType::kString, string_value, + initial_ctx_, client_metadata_, provider_metadata_, + hook_data_); + StringFlagEvaluationDetails details("string-flag", "variant-val", + Reason::kTargetingMatch, "v1", + FlagMetadata()); + HookHints hints{{"after_hint", std::any(std::string("after-data"))}}; + + hook.After(ctx, details, hints); + + EXPECT_TRUE(hook.after_called); + EXPECT_EQ(hook.last_flag_key, "string-flag"); + EXPECT_EQ(hook.last_reason, Reason::kTargetingMatch); + EXPECT_EQ(hook.last_hint_value, "after-data"); +} + +TEST_F(HookTest, OverriddenErrorReceivesExceptionAndHints) { + constexpr int64_t kIntValue = 42; + TrackingHook hook; + IntHookContext ctx("int-flag", FlagValueType::kInteger, kIntValue, + initial_ctx_, client_metadata_, provider_metadata_, + hook_data_); + std::runtime_error error("provider timeout error"); + HookHints hints{{"error_hint", std::any(std::string("error-data"))}}; + + hook.Error(ctx, error, hints); + + EXPECT_TRUE(hook.error_called); + EXPECT_EQ(hook.last_flag_key, "int-flag"); + EXPECT_EQ(hook.last_error_message, "provider timeout error"); + EXPECT_EQ(hook.last_hint_value, "error-data"); +} + +TEST_F(HookTest, OverriddenFinallyReceivesContextDetailsAndHints) { + constexpr double kDoubleValue = 1.0; + constexpr double kSecondDoubleValue = 2.718; + TrackingHook hook; + DoubleHookContext ctx("double-flag", FlagValueType::kDouble, kDoubleValue, + initial_ctx_, client_metadata_, provider_metadata_, + hook_data_); + DoubleFlagEvaluationDetails details("double-flag", kSecondDoubleValue, + Reason::kCached, std::nullopt, + FlagMetadata()); + HookHints hints{{"finally_hint", std::any(std::string("finally-data"))}}; + + hook.Finally(ctx, details, hints); + + EXPECT_TRUE(hook.finally_called); + EXPECT_EQ(hook.last_flag_key, "double-flag"); + EXPECT_EQ(hook.last_reason, Reason::kCached); + EXPECT_EQ(hook.last_hint_value, "finally-data"); +} + +TEST_F(HookTest, PolymorphicDestructionViaGeneralHookPointer) { + std::vector> hooks; + hooks.push_back(std::make_unique()); + hooks.push_back(std::make_unique()); + hooks.push_back(std::make_unique()); + hooks.push_back(std::make_unique()); + hooks.push_back(std::make_unique()); + + EXPECT_EQ(hooks.size(), 5); +} + +TEST_F(HookTest, PolymorphicDispatchViaGeneralHookReference) { + TrackingHook hook; + GeneralHook& general_hook = hook; + + BoolHookContext ctx("bool-flag", FlagValueType::kBoolean, true, initial_ctx_, + client_metadata_, provider_metadata_, hook_data_); + BoolFlagEvaluationDetails details("bool-flag", true, Reason::kStatic, + std::nullopt, FlagMetadata()); + HookHints hints; + + general_hook.Before(ctx, hints); + general_hook.After(ctx, details, hints); + + EXPECT_TRUE(hook.before_called); + EXPECT_TRUE(hook.after_called); + EXPECT_EQ(hook.last_flag_key, "bool-flag"); +} + +TEST_F(HookTest, TypedHookIgnoresMismatchedFlagTypes) { + TrackingHook bool_hook; + GeneralHook& general_hook = bool_hook; + + StringHookContext string_ctx("string-flag", FlagValueType::kString, "default", + initial_ctx_, client_metadata_, + provider_metadata_, hook_data_); + StringFlagEvaluationDetails details("string-flag", "val", Reason::kStatic, + std::nullopt, FlagMetadata()); + HookHints hints; + + std::optional res = general_hook.Before(string_ctx, hints); + general_hook.After(string_ctx, details, hints); + + EXPECT_FALSE(res.has_value()); + EXPECT_FALSE(bool_hook.before_called); + EXPECT_FALSE(bool_hook.after_called); +} + +} // namespace openfeature