diff --git a/openfeature/BUILD b/openfeature/BUILD index 8c2653c..c36e83a 100644 --- a/openfeature/BUILD +++ b/openfeature/BUILD @@ -55,6 +55,28 @@ cc_library( ], ) +cc_library( + name = "hook_support", + srcs = ["hook_support.cpp"], + hdrs = ["hook_support.h"], + include_prefix = "openfeature", + deps = [ + ":error_code", + ":evaluation_context", + ":evaluation_options", + ":flag_evaluation_details", + ":flag_type_value", + ":general_hook", + ":global_hook_manager", + ":hook_context", + ":hook_data", + ":hook_hints", + ":metadata", + ":provider", + ":reason", + ], +) + cc_library( name = "client_api", srcs = ["client_api.cpp"], @@ -63,11 +85,18 @@ cc_library( deps = [ ":client", ":evaluation_context", + ":evaluation_options", ":features", ":flag_evaluation_details", ":flag_metadata", + ":flag_type_value", ":general_hook", ":global_context_manager", + ":global_hook_manager", + ":hook_context", + ":hook_data", + ":hook_hints", + ":hook_support", ":metadata", ":provider", ":provider_repository", @@ -164,6 +193,16 @@ cc_library( ], ) +cc_library( + name = "global_hook_manager", + srcs = ["global_hook_manager.cpp"], + hdrs = ["global_hook_manager.h"], + include_prefix = "openfeature", + deps = [ + ":general_hook", + ], +) + cc_library( name = "hook_context", srcs = ["hook_context.cpp"], @@ -227,6 +266,7 @@ cc_library( ":client_api", ":evaluation_context", ":global_context_manager", + ":global_hook_manager", ":metadata", ":openfeature", ":provider", diff --git a/openfeature/client_api.cpp b/openfeature/client_api.cpp index 4071757..1ba00f9 100644 --- a/openfeature/client_api.cpp +++ b/openfeature/client_api.cpp @@ -3,6 +3,7 @@ #include #include "openfeature/flag_metadata.h" +#include "openfeature/flag_type_value.h" #include "openfeature/global_context_manager.h" #include "openfeature/reason.h" @@ -306,7 +307,7 @@ std::unique_ptr ClientAPI::EvaluateBooleanFlag( const std::optional& ctx, const std::optional& options) { return this->EvaluateFlag( - default_value, ctx, options, + flag_key, FlagValueType::kBoolean, default_value, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetBooleanEvaluation(flag_key, default_value, @@ -320,7 +321,7 @@ std::unique_ptr ClientAPI::EvaluateStringFlag( const std::optional& options) { std::string default_str(default_value); return this->EvaluateFlag( - default_str, ctx, options, + flag_key, FlagValueType::kString, default_str, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetStringEvaluation(flag_key, default_value, @@ -333,7 +334,7 @@ std::unique_ptr ClientAPI::EvaluateIntegerFlag( const std::optional& ctx, const std::optional& options) { return this->EvaluateFlag( - default_value, ctx, options, + flag_key, FlagValueType::kInteger, default_value, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetIntegerEvaluation(flag_key, default_value, @@ -346,7 +347,7 @@ std::unique_ptr ClientAPI::EvaluateDoubleFlag( const std::optional& ctx, const std::optional& options) { return this->EvaluateFlag( - default_value, ctx, options, + flag_key, FlagValueType::kDouble, default_value, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetDoubleEvaluation(flag_key, default_value, @@ -359,7 +360,7 @@ std::unique_ptr ClientAPI::EvaluateObjectFlag( const std::optional& ctx, const std::optional& options) { return this->EvaluateFlag( - default_value, ctx, options, + flag_key, FlagValueType::kObject, default_value, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetObjectEvaluation(flag_key, default_value, diff --git a/openfeature/client_api.h b/openfeature/client_api.h index dec0ad3..e0fb44d 100644 --- a/openfeature/client_api.h +++ b/openfeature/client_api.h @@ -1,19 +1,31 @@ #ifndef CPP_SDK_INCLUDE_OPENFEATURE_CLIENT_API_H_ #define CPP_SDK_INCLUDE_OPENFEATURE_CLIENT_API_H_ +#include #include #include #include +#include +#include #include #include +#include +#include #include #include "openfeature/client.h" #include "openfeature/evaluation_context.h" +#include "openfeature/evaluation_options.h" #include "openfeature/features.h" #include "openfeature/flag_evaluation_details.h" +#include "openfeature/flag_type_value.h" #include "openfeature/general_hook.h" #include "openfeature/global_context_manager.h" +#include "openfeature/global_hook_manager.h" +#include "openfeature/hook_context.h" +#include "openfeature/hook_data.h" +#include "openfeature/hook_hints.h" +#include "openfeature/hook_support.h" #include "openfeature/metadata.h" #include "openfeature/provider.h" #include "openfeature/provider_repository.h" @@ -178,6 +190,7 @@ class ClientAPI : public Client { template std::unique_ptr EvaluateFlag( + std::string_view flag_key, FlagValueType flag_type, ValueType default_value, const std::optional& ctx, const std::optional& options, ProviderCallable provider_call); @@ -210,6 +223,17 @@ class ClientAPI : public Client { EvaluationContext MergeContexts( const std::optional& invocation_ctx); + template + void ResolveProvider( + const std::shared_ptr& provider, + const std::shared_ptr& manager, + ProviderStatus provider_status, const EvaluationContext& merged_context, + std::string_view flag_key, ProviderCallable& provider_call, + std::unique_ptr>& evaluation_details, + std::optional& error_code, std::string& error_message, + std::unique_ptr& captured_exception, + bool& has_error) const; + ProviderRepository& provider_repository_; std::string domain_; EvaluationContext evaluation_context_; @@ -218,65 +242,177 @@ class ClientAPI : public Client { std::vector> hooks_; }; +template +void ClientAPI::ResolveProvider( + const std::shared_ptr& provider, + const std::shared_ptr& manager, + ProviderStatus provider_status, const EvaluationContext& merged_context, + std::string_view flag_key, ProviderCallable& provider_call, + std::unique_ptr>& evaluation_details, + std::optional& error_code, std::string& error_message, + std::unique_ptr& captured_exception, + bool& has_error) const { + if (!manager) { + has_error = true; + error_code = ErrorCode::kGeneral; + error_message = "Provider status manager not found for domain"; + captured_exception = std::make_unique(error_message); + } else if (provider_status == ProviderStatus::kNotReady) { + has_error = true; + error_code = ErrorCode::kProviderNotReady; + error_message = "Provider is not ready"; + captured_exception = std::make_unique(error_message); + } else if (provider_status == ProviderStatus::kFatal) { + has_error = true; + error_code = ErrorCode::kProviderFatal; + error_message = "Provider is in fatal error state"; + captured_exception = std::make_unique(error_message); + } else if (!provider) { + has_error = true; + error_code = ErrorCode::kProviderFatal; + error_message = "Provider not found for domain"; + captured_exception = std::make_unique(error_message); + } else { + try { + auto result = provider_call(provider, merged_context); + if (!result.ok()) { + has_error = true; + error_code = ErrorCode::kGeneral; + error_message = std::string(result.status().message()); + captured_exception = + std::make_unique(error_message); + } else if (*result == nullptr) { + has_error = true; + error_code = ErrorCode::kGeneral; + error_message = "Provider returned null resolution details"; + captured_exception = + std::make_unique(error_message); + } else if ((*result)->GetErrorCode().has_value()) { + has_error = true; + error_code = (*result)->GetErrorCode(); + error_message = (*result)->GetErrorMessage().value_or("Provider error"); + captured_exception = + std::make_unique(error_message); + } else { + evaluation_details = std::make_unique>( + std::string(flag_key), **result); + } + } catch (const std::exception& exception) { + has_error = true; + error_code = ErrorCode::kGeneral; + error_message = + std::string("Exception during evaluation: ") + exception.what(); + captured_exception = + std::make_unique(exception.what()); + } catch (...) { + has_error = true; + error_code = ErrorCode::kGeneral; + error_message = "Unknown exception during evaluation"; + captured_exception = std::make_unique(error_message); + } + } +} + template std::unique_ptr ClientAPI::EvaluateFlag( - ValueType default_value, const std::optional& ctx, + std::string_view flag_key, FlagValueType flag_type, ValueType default_value, + const std::optional& ctx, const std::optional& options, ProviderCallable provider_call) { std::shared_ptr manager = provider_repository_.GetFeatureProviderStatusManager(domain_); - if (!manager) { - return std::make_unique( - default_value, Reason::kError, std::nullopt, FlagMetadata(), - ErrorCode::kGeneral, "Provider status manager not found for domain"); - } + ProviderStatus provider_status = + manager ? manager->GetStatus() : ProviderStatus::kNotReady; + bool is_provider_ready = + (manager != nullptr && provider_status != ProviderStatus::kNotReady && + provider_status != ProviderStatus::kFatal); + std::shared_ptr provider = + (is_provider_ready && manager) ? manager->GetProvider() : nullptr; + + // Collect hooks in order of increasing specificity + std::vector> forward_hooks = + HookSupport::CollectHooks(GetHooks(), options, provider); + + // Reverse list for after, error, and finally stages + std::vector> reverse_hooks( + forward_hooks.rbegin(), forward_hooks.rend()); + + HookHints hints = options.has_value() ? options->hook_hints : HookHints{}; + auto hook_data_map = HookSupport::CreateHookDataMap(forward_hooks); + + Metadata client_metadata = GetMetadata(); + Metadata provider_metadata = + provider ? provider->GetMetadata() : Metadata{""}; + + // Initialize merged context: Global -> Client -> Invocation + EvaluationContext merged_context = MergeContexts(ctx); - ProviderStatus status = manager->GetStatus(); - if (status == ProviderStatus::kNotReady) { - return std::make_unique( - default_value, Reason::kError, std::nullopt, FlagMetadata(), - ErrorCode::kProviderNotReady, "Provider is not ready"); + bool has_error = false; + std::string error_message; + std::optional error_code = std::nullopt; + std::unique_ptr captured_exception; + std::unique_ptr> evaluation_details; + + // Before Stage + if (!HookSupport::ExecuteBeforeHooks( + forward_hooks, flag_key, flag_type, default_value, client_metadata, + provider_metadata, hints, hook_data_map, merged_context, error_code, + error_message, captured_exception)) { + has_error = true; } - if (status == ProviderStatus::kFatal) { - return std::make_unique( - default_value, Reason::kError, std::nullopt, FlagMetadata(), - ErrorCode::kProviderFatal, "Provider is in fatal error state"); + + // Provider Resolution stage (only if no error in Before) + if (!has_error) { + ResolveProvider(provider, manager, provider_status, merged_context, + flag_key, provider_call, evaluation_details, error_code, + error_message, captured_exception, has_error); } - std::shared_ptr provider = manager->GetProvider(); - if (!provider) { - return std::make_unique( - default_value, Reason::kError, std::nullopt, FlagMetadata(), - ErrorCode::kProviderFatal, "Provider not found for domain"); + // Construct error evaluation_details if error occurred in Before or + // Resolution + if (has_error && !evaluation_details) { + evaluation_details = std::make_unique>( + std::string(flag_key), default_value, Reason::kError, std::nullopt, + FlagMetadata(), error_code.value_or(ErrorCode::kGeneral), + error_message); } - EvaluationContext merged_context = MergeContexts(ctx); + // After stage (only if no error occurred) + if (!has_error && evaluation_details) { + HookSupport::ExecuteAfterHooks( + reverse_hooks, flag_key, flag_type, default_value, merged_context, + client_metadata, provider_metadata, hints, hook_data_map, + evaluation_details, error_code, error_message, captured_exception, + has_error); + } - try { - auto result = provider_call(provider, merged_context); + // Error stage + if (has_error && captured_exception) { + HookSupport::ExecuteErrorHooks(reverse_hooks, flag_key, flag_type, + default_value, merged_context, + client_metadata, provider_metadata, hints, + hook_data_map, *captured_exception); + } - if (!result.ok()) { - return std::make_unique( - default_value, Reason::kError, std::nullopt, FlagMetadata(), - ErrorCode::kGeneral, std::string(result.status().message())); - } - if (*result == nullptr) { - return std::make_unique( - default_value, Reason::kError, std::nullopt, FlagMetadata(), - ErrorCode::kGeneral, "Provider returned null resolution details"); - } - return std::move(*result); - } catch (const std::exception& e) { - return std::make_unique( - default_value, Reason::kError, std::nullopt, FlagMetadata(), - ErrorCode::kGeneral, - std::string("Exception during evaluation: ") + e.what()); - } catch (...) { - return std::make_unique( - default_value, Reason::kError, std::nullopt, FlagMetadata(), - ErrorCode::kGeneral, "Unknown exception during evaluation"); + // Finally stage (always executed) + if (!evaluation_details) { + evaluation_details = std::make_unique>( + std::string(flag_key), default_value, Reason::kError, std::nullopt, + FlagMetadata(), error_code.value_or(ErrorCode::kGeneral), + error_message); } + + HookSupport::ExecuteFinallyHooks(reverse_hooks, flag_key, flag_type, + default_value, merged_context, + client_metadata, provider_metadata, hints, + hook_data_map, *evaluation_details); + + return std::make_unique( + evaluation_details->GetValue(), evaluation_details->GetReason(), + evaluation_details->GetVariant(), evaluation_details->GetFlagMetadata(), + evaluation_details->GetErrorCode(), + evaluation_details->GetErrorMessage()); } } // namespace openfeature diff --git a/openfeature/global_hook_manager.cpp b/openfeature/global_hook_manager.cpp new file mode 100644 index 0000000..ffe6afe --- /dev/null +++ b/openfeature/global_hook_manager.cpp @@ -0,0 +1,45 @@ +#include "openfeature/global_hook_manager.h" + +#include +#include +#include +#include +#include + +#include "openfeature/general_hook.h" + +namespace openfeature { + +GlobalHookManager& GlobalHookManager::GetInstance() { + static GlobalHookManager instance; + return instance; +} + +void GlobalHookManager::AddHooks( + std::vector> hooks) { + std::unique_lock lock(hooks_mutex_); + hooks_.reserve(hooks_.size() + hooks.size()); + for (auto& hook : hooks) { + if (hook != nullptr) { + hooks_.push_back(std::move(hook)); + } + } +} + +void GlobalHookManager::AddHook(std::shared_ptr hook) { + if (hook == nullptr) return; + std::unique_lock lock(hooks_mutex_); + hooks_.push_back(std::move(hook)); +} + +std::vector> GlobalHookManager::GetHooks() const { + std::shared_lock lock(hooks_mutex_); + return hooks_; +} + +void GlobalHookManager::ClearHooks() { + std::unique_lock lock(hooks_mutex_); + hooks_.clear(); +} + +} // namespace openfeature diff --git a/openfeature/global_hook_manager.h b/openfeature/global_hook_manager.h new file mode 100644 index 0000000..0365fde --- /dev/null +++ b/openfeature/global_hook_manager.h @@ -0,0 +1,42 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_GLOBAL_HOOK_MANAGER_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_GLOBAL_HOOK_MANAGER_H_ + +#include +#include +#include + +#include "openfeature/general_hook.h" + +namespace openfeature { + +// Manages global hooks for the OpenFeature SDK. +// Provides thread-safe hook storage and access across the application. +class GlobalHookManager { + public: + static GlobalHookManager& GetInstance(); + + GlobalHookManager(const GlobalHookManager&) = delete; + GlobalHookManager& operator=(const GlobalHookManager&) = delete; + + // Adds one or more global hooks, appending them to existing hooks. + // Filters out nullptr entries. + void AddHooks(std::vector> hooks); + + // Adds a single global hook. Filters out nullptr entries. + void AddHook(std::shared_ptr hook); + + // Retrieves all configured global hooks. + std::vector> GetHooks() const; + + // Clears all configured global hooks. + void ClearHooks(); + + private: + GlobalHookManager() = default; + mutable std::shared_mutex hooks_mutex_; + std::vector> hooks_; +}; + +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_GLOBAL_HOOK_MANAGER_H_ diff --git a/openfeature/hook_data.h b/openfeature/hook_data.h index fb8989a..0732752 100644 --- a/openfeature/hook_data.h +++ b/openfeature/hook_data.h @@ -28,6 +28,15 @@ class HookData { return nullptr; } + template + const T* GetAs(std::string_view key) const { + auto it_key = data_.find(std::string(key)); + if (it_key != data_.end()) { + return std::any_cast(&it_key->second); + } + return nullptr; + } + private: std::unordered_map data_; }; diff --git a/openfeature/hook_support.cpp b/openfeature/hook_support.cpp new file mode 100644 index 0000000..fd413a1 --- /dev/null +++ b/openfeature/hook_support.cpp @@ -0,0 +1,58 @@ +#include "openfeature/hook_support.h" + +namespace openfeature { + +std::vector> HookSupport::CollectHooks( + const std::vector>& client_hooks, + const std::optional& options, + const std::shared_ptr& provider) { + std::vector> forward_hooks; + auto api_hooks = GlobalHookManager::GetInstance().GetHooks(); + auto invocation_hooks = options.has_value() + ? options->hooks + : std::vector>{}; + auto provider_hooks = provider ? provider->GetHooks() + : std::vector>{}; + + forward_hooks.reserve(api_hooks.size() + client_hooks.size() + + invocation_hooks.size() + provider_hooks.size()); + + for (auto& hook : api_hooks) { + if (hook != nullptr) { + forward_hooks.push_back(std::move(hook)); + } + } + for (const auto& hook : client_hooks) { + if (hook != nullptr) { + forward_hooks.push_back(hook); + } + } + for (auto& hook : invocation_hooks) { + if (hook != nullptr) { + forward_hooks.push_back(std::move(hook)); + } + } + for (auto& hook : provider_hooks) { + if (hook != nullptr) { + forward_hooks.push_back(std::move(hook)); + } + } + + return forward_hooks; +} + +std::unordered_map> +HookSupport::CreateHookDataMap( + const std::vector>& hooks) { + std::unordered_map> + hook_data_map; + for (const auto& hook : hooks) { + if (hook != nullptr && + hook_data_map.find(hook.get()) == hook_data_map.end()) { + hook_data_map[hook.get()] = std::make_shared(); + } + } + return hook_data_map; +} + +} // namespace openfeature diff --git a/openfeature/hook_support.h b/openfeature/hook_support.h new file mode 100644 index 0000000..13299c4 --- /dev/null +++ b/openfeature/hook_support.h @@ -0,0 +1,181 @@ +#ifndef OPENFEATURE_HOOK_SUPPORT_H_ +#define OPENFEATURE_HOOK_SUPPORT_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openfeature/error_code.h" +#include "openfeature/evaluation_context.h" +#include "openfeature/evaluation_options.h" +#include "openfeature/flag_evaluation_details.h" +#include "openfeature/flag_type_value.h" +#include "openfeature/general_hook.h" +#include "openfeature/global_hook_manager.h" +#include "openfeature/hook_context.h" +#include "openfeature/hook_data.h" +#include "openfeature/hook_hints.h" +#include "openfeature/metadata.h" +#include "openfeature/provider.h" +#include "openfeature/reason.h" + +namespace openfeature { + +// Helper class responsible for hook collection +// and lifecycle stage execution (before, after, error, finally). +class HookSupport { + public: + // Collects hooks across API, Client, Invocation, and Provider tiers in FIFO + // order. + static std::vector> CollectHooks( + const std::vector>& client_hooks, + const std::optional& options, + const std::shared_ptr& provider); + + // Allocates an HookData instance for each unique hook instance. + static std::unordered_map> + CreateHookDataMap(const std::vector>& hooks); + + // Executes before hooks in forward order (API -> Client -> Invocation -> + // Provider). + template + static bool ExecuteBeforeHooks( + const std::vector>& hooks, + std::string_view flag_key, FlagValueType flag_type, + const ValueType& default_value, const Metadata& client_metadata, + const Metadata& provider_metadata, const HookHints& hints, + std::unordered_map>& + hook_data_map, + EvaluationContext& merged_context, std::optional& error_code, + std::string& error_message, + std::unique_ptr& captured_exception) { + for (const auto& hook : hooks) { + try { + HookContext hook_ctx( + std::string(flag_key), flag_type, default_value, merged_context, + client_metadata, provider_metadata, hook_data_map[hook.get()]); + auto modified_ctx = hook->Before(hook_ctx, hints); + if (modified_ctx.has_value()) { + merged_context = + EvaluationContext::Merge({&merged_context, &(*modified_ctx)}); + } + } catch (const std::exception& exception) { + error_code = ErrorCode::kGeneral; + error_message = exception.what(); + captured_exception = + std::make_unique(exception.what()); + return false; + } catch (...) { + error_code = ErrorCode::kGeneral; + error_message = "Unknown exception in before hook"; + captured_exception = + std::make_unique(error_message); + return false; + } + } + return true; + } + + // Executes after hooks in reverse order (Provider -> Invocation -> Client -> + // API). + template + static void ExecuteAfterHooks( + const std::vector>& reverse_hooks, + std::string_view flag_key, FlagValueType flag_type, + const ValueType& default_value, const EvaluationContext& merged_context, + const Metadata& client_metadata, const Metadata& provider_metadata, + const HookHints& hints, + std::unordered_map>& + hook_data_map, + std::unique_ptr>& evaluation_details, + std::optional& error_code, std::string& error_message, + std::unique_ptr& captured_exception, bool& has_error) { + for (const auto& hook : reverse_hooks) { + try { + HookContext hook_ctx( + std::string(flag_key), flag_type, default_value, merged_context, + client_metadata, provider_metadata, hook_data_map[hook.get()]); + hook->After(hook_ctx, *evaluation_details, hints); + } catch (const std::exception& exception) { + has_error = true; + error_code = ErrorCode::kGeneral; + error_message = exception.what(); + captured_exception = + std::make_unique(exception.what()); + evaluation_details = std::make_unique>( + std::string(flag_key), default_value, Reason::kError, std::nullopt, + FlagMetadata(), error_code.value_or(ErrorCode::kGeneral), + error_message); + break; + } catch (...) { + has_error = true; + error_code = ErrorCode::kGeneral; + error_message = "Unknown exception in after hook"; + captured_exception = + std::make_unique(error_message); + evaluation_details = std::make_unique>( + std::string(flag_key), default_value, Reason::kError, std::nullopt, + FlagMetadata(), error_code.value_or(ErrorCode::kGeneral), + error_message); + break; + } + } + } + + // Executes error hooks in reverse order (Provider -> Invocation -> Client -> + // API). + template + static void ExecuteErrorHooks( + const std::vector>& reverse_hooks, + std::string_view flag_key, FlagValueType flag_type, + const ValueType& default_value, const EvaluationContext& merged_context, + const Metadata& client_metadata, const Metadata& provider_metadata, + const HookHints& hints, + std::unordered_map>& + hook_data_map, + const std::exception& captured_exception) { + for (const auto& hook : reverse_hooks) { + try { + HookContext hook_ctx( + std::string(flag_key), flag_type, default_value, merged_context, + client_metadata, provider_metadata, hook_data_map[hook.get()]); + hook->Error(hook_ctx, captured_exception, hints); + } catch (...) { + // evaluation must proceed + } + } + } + + // Executes finally hooks in reverse order (Provider -> Invocation -> Client + // -> API). + template + static void ExecuteFinallyHooks( + const std::vector>& reverse_hooks, + std::string_view flag_key, FlagValueType flag_type, + const ValueType& default_value, const EvaluationContext& merged_context, + const Metadata& client_metadata, const Metadata& provider_metadata, + const HookHints& hints, + std::unordered_map>& + hook_data_map, + const FlagEvaluationDetails& evaluation_details) { + for (const auto& hook : reverse_hooks) { + try { + HookContext hook_ctx( + std::string(flag_key), flag_type, default_value, merged_context, + client_metadata, provider_metadata, hook_data_map[hook.get()]); + hook->Finally(hook_ctx, evaluation_details, hints); + } catch (...) { + // evaluation must proceed + } + } + } +}; + +} // namespace openfeature + +#endif // OPENFEATURE_HOOK_SUPPORT_H_ diff --git a/openfeature/openfeature_api.cpp b/openfeature/openfeature_api.cpp index e407161..856f4e6 100644 --- a/openfeature/openfeature_api.cpp +++ b/openfeature/openfeature_api.cpp @@ -2,6 +2,7 @@ #include "openfeature/client_api.h" #include "openfeature/global_context_manager.h" +#include "openfeature/global_hook_manager.h" namespace openfeature { @@ -86,30 +87,20 @@ ProviderStatus OpenFeatureAPI::GetProviderStatus( } void OpenFeatureAPI::AddHooks(std::vector> hooks) { - std::unique_lock lock(hooks_mutex_); - hooks_.reserve(hooks_.size() + hooks.size()); - for (auto& hook : hooks) { - if (hook != nullptr) { - hooks_.push_back(std::move(hook)); - } - } + GlobalHookManager::GetInstance().AddHooks(std::move(hooks)); } void OpenFeatureAPI::AddHook(std::shared_ptr hook) { - if (hook == nullptr) return; - std::unique_lock lock(hooks_mutex_); - hooks_.push_back(std::move(hook)); + GlobalHookManager::GetInstance().AddHook(std::move(hook)); } std::vector> OpenFeatureAPI::GetHooks() const { - std::shared_lock lock(hooks_mutex_); - return hooks_; + return GlobalHookManager::GetInstance().GetHooks(); } void OpenFeatureAPI::Shutdown() { provider_repository_.Shutdown(); - std::unique_lock lock(hooks_mutex_); - hooks_.clear(); + GlobalHookManager::GetInstance().ClearHooks(); } } // namespace openfeature \ No newline at end of file diff --git a/openfeature/openfeature_api.h b/openfeature/openfeature_api.h index d2a8fc6..ba96aec 100644 --- a/openfeature/openfeature_api.h +++ b/openfeature/openfeature_api.h @@ -10,6 +10,7 @@ #include "openfeature/evaluation_context.h" #include "openfeature/general_hook.h" #include "openfeature/global_context_manager.h" +#include "openfeature/global_hook_manager.h" #include "openfeature/metadata.h" #include "openfeature/openfeature.h" #include "openfeature/provider.h" @@ -85,8 +86,6 @@ class OpenFeatureAPI : public OpenFeature { private: ProviderRepository provider_repository_; - mutable std::shared_mutex hooks_mutex_; - std::vector> hooks_; OpenFeatureAPI(); }; diff --git a/test/BUILD b/test/BUILD index 70ba6f5..ce15632 100644 --- a/test/BUILD +++ b/test/BUILD @@ -21,7 +21,9 @@ cc_test( ":mock_feature_provider", "//openfeature:client_api", "//openfeature:flag_evaluation_details", + "//openfeature:global_hook_manager", "//openfeature:hook", + "//openfeature:hook_support", "@googletest//:gtest_main", ], ) @@ -103,6 +105,17 @@ cc_test( ], ) +cc_test( + name = "global_hook_manager_test", + srcs = ["global_hook_manager_test.cpp"], + deps = [ + "//openfeature:general_hook", + "//openfeature:global_hook_manager", + "//openfeature:hook", + "@googletest//:gtest_main", + ], +) + cc_test( name = "value_test", srcs = ["value_test.cpp"], diff --git a/test/client_api_test.cpp b/test/client_api_test.cpp index 28acf18..3048490 100644 --- a/test/client_api_test.cpp +++ b/test/client_api_test.cpp @@ -15,7 +15,9 @@ #include "openfeature/evaluation_context.h" #include "openfeature/evaluation_options.h" #include "openfeature/global_context_manager.h" +#include "openfeature/global_hook_manager.h" #include "openfeature/hook.h" +#include "openfeature/hook_support.h" #include "openfeature/provider_status.h" using ::openfeature::BoolFlagEvaluationDetails; @@ -27,6 +29,8 @@ using ::openfeature::EvaluationContext; using ::openfeature::EvaluationOptions; using ::openfeature::FlagMetadata; using ::openfeature::GlobalContextManager; +using ::openfeature::GlobalHookManager; +using ::openfeature::HookSupport; using ::openfeature::IntFlagEvaluationDetails; using ::openfeature::Metadata; using ::openfeature::MockFeatureProvider; @@ -46,10 +50,14 @@ using ::testing::StrictMock; class ClientAPITest : public ::testing::Test { protected: void SetUp() override { - // Reset the Global Context to a clean state before each test. + // Reset Global Context and Global Hooks to clean states before each test. GlobalContextManager::GetInstance().SetGlobalEvaluationContext( EvaluationContext::Builder().build()); + GlobalHookManager::GetInstance().ClearHooks(); } + + void TearDown() override { GlobalHookManager::GetInstance().ClearHooks(); } + ProviderRepository repo_; }; @@ -392,8 +400,8 @@ TEST_F(ClientAPITest, ContextMergingPrecedence) { .WithAttribute("shared_attr_gci", "global_shared_gci") .build()); - std::shared_ptr> mock_provider = - std::make_shared>(); + std::shared_ptr> mock_provider = + std::make_shared>(); EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); EXPECT_CALL(*mock_provider, Shutdown()).WillOnce(Return(absl::OkStatus())); @@ -773,3 +781,681 @@ TEST_F(ClientAPITest, AddHookAndAddHooksFiltersNullptrs) { ASSERT_EQ(hooks.size(), 1); EXPECT_EQ(hooks[0], valid_hook); } + +namespace { + +// Helper hook that logs lifecycle method calls +class OrderTrackingHook : public openfeature::BoolHook { + public: + explicit OrderTrackingHook(std::string name, + std::vector& execution_log) + : name_(std::move(name)), execution_log_(execution_log) {} + + std::optional Before( + const openfeature::HookContext& /*ctx*/, + const openfeature::HookHints& /*hints*/) override { + execution_log_.push_back("before:" + name_); + return std::nullopt; + } + + void After(const openfeature::HookContext& /*ctx*/, + const openfeature::FlagEvaluationDetails& /*details*/, + const openfeature::HookHints& /*hints*/) override { + execution_log_.push_back("after:" + name_); + } + + void Error(const openfeature::HookContext& /*ctx*/, + const std::exception& /*exception*/, + const openfeature::HookHints& /*hints*/) override { + execution_log_.push_back("error:" + name_); + } + + void Finally(const openfeature::HookContext& /*ctx*/, + const openfeature::FlagEvaluationDetails& /*details*/, + const openfeature::HookHints& /*hints*/) override { + execution_log_.push_back("finally:" + name_); + } + + private: + std::string name_; + std::vector& execution_log_; +}; + +// Helper hook that mutates context in Before +class ContextMutatingHook : public openfeature::BoolHook { + public: + explicit ContextMutatingHook(std::string key, std::string value) + : key_(std::move(key)), value_(std::move(value)) {} + + std::optional Before( + const openfeature::HookContext& /*ctx*/, + const openfeature::HookHints& /*hints*/) override { + return EvaluationContext::Builder().WithAttribute(key_, value_).build(); + } + + private: + std::string key_; + std::string value_; +}; + +// Helper hook to test HookData isolation and persistence +class HookDataTestHook : public openfeature::BoolHook { + public: + explicit HookDataTestHook(std::string hook_id) + : hook_id_(std::move(hook_id)) {} + + std::optional Before( + const openfeature::HookContext& ctx, + const openfeature::HookHints& /*hints*/) override { + ctx.GetHookData()->Set("id", hook_id_); + return std::nullopt; + } + + void After(const openfeature::HookContext& ctx, + const openfeature::FlagEvaluationDetails& /*details*/, + const openfeature::HookHints& /*hints*/) override { + const auto* stored_id = ctx.GetHookData()->GetAs("id"); + if (stored_id != nullptr) { + after_id_ = *stored_id; + } + } + + void Finally(const openfeature::HookContext& ctx, + const openfeature::FlagEvaluationDetails& /*details*/, + const openfeature::HookHints& /*hints*/) override { + const auto* stored_id = ctx.GetHookData()->GetAs("id"); + if (stored_id != nullptr) { + finally_id_ = *stored_id; + } + } + + std::string after_id_; + std::string finally_id_; + + private: + std::string hook_id_; +}; + +// Helper hook to test HookHints +class HintsTrackingHook : public openfeature::BoolHook { + public: + std::optional Before( + const openfeature::HookContext& /*ctx*/, + const openfeature::HookHints& hints) override { + auto iterator = hints.find("hint_key"); + if (iterator != hints.end()) { + const auto* value = std::any_cast(&iterator->second); + if (value != nullptr) { + before_hint_ = *value; + } + } + return std::nullopt; + } + + void After(const openfeature::HookContext& /*ctx*/, + const openfeature::FlagEvaluationDetails& /*details*/, + const openfeature::HookHints& hints) override { + auto iterator = hints.find("hint_key"); + if (iterator != hints.end()) { + const auto* value = std::any_cast(&iterator->second); + if (value != nullptr) { + after_hint_ = *value; + } + } + } + + void Finally(const openfeature::HookContext& /*ctx*/, + const openfeature::FlagEvaluationDetails& /*details*/, + const openfeature::HookHints& hints) override { + auto iterator = hints.find("hint_key"); + if (iterator != hints.end()) { + const auto* value = std::any_cast(&iterator->second); + if (value != nullptr) { + finally_hint_ = *value; + } + } + } + + std::string before_hint_; + std::string after_hint_; + std::string finally_hint_; +}; + +enum class ThrowStage { kBefore, kAfter, kError, kFinally }; + +class ThrowingHook : public openfeature::BoolHook { + public: + explicit ThrowingHook(ThrowStage stage, std::string message, + std::vector& execution_log) + : stage_(stage), + message_(std::move(message)), + execution_log_(execution_log) {} + + std::optional Before( + const openfeature::HookContext& /*ctx*/, + const openfeature::HookHints& /*hints*/) override { + execution_log_.push_back("before"); + if (stage_ == ThrowStage::kBefore) { + throw std::runtime_error(message_); + } + return std::nullopt; + } + + void After(const openfeature::HookContext& /*ctx*/, + const openfeature::FlagEvaluationDetails& /*details*/, + const openfeature::HookHints& /*hints*/) override { + execution_log_.push_back("after"); + if (stage_ == ThrowStage::kAfter) { + throw std::runtime_error(message_); + } + } + + void Error(const openfeature::HookContext& /*ctx*/, + const std::exception& /*exception*/, + const openfeature::HookHints& /*hints*/) override { + execution_log_.push_back("error"); + if (stage_ == ThrowStage::kError) { + throw std::runtime_error(message_); + } + } + + void Finally(const openfeature::HookContext& /*ctx*/, + const openfeature::FlagEvaluationDetails& /*details*/, + const openfeature::HookHints& /*hints*/) override { + execution_log_.push_back("finally"); + if (stage_ == ThrowStage::kFinally) { + throw std::runtime_error(message_); + } + } + + private: + ThrowStage stage_; + std::string message_; + std::vector& execution_log_; +}; + +} // namespace + +// Test full 4-tier hook execution order on success: +// Before: API -> Client -> Invocation -> Provider +// After: Provider -> Invocation -> Client -> API +// Finally: Provider -> Invocation -> Client -> API +TEST_F(ClientAPITest, HooksExecuteInCorrectOrderOnSuccess) { + std::string domain = "order-domain"; + auto mock_provider = std::make_shared>(); + ON_CALL(*mock_provider, Init(_)).WillByDefault(Return(absl::OkStatus())); + ON_CALL(*mock_provider, GetBooleanEvaluation(_, _, _)) + .WillByDefault(testing::Invoke( + [](std::string_view, bool, const EvaluationContext&) + -> absl::StatusOr> { + return std::make_unique( + true, Reason::kTargetingMatch, std::nullopt, FlagMetadata()); + })); + + std::vector execution_log; + auto api_hook = std::make_shared("api", execution_log); + auto client_hook = + std::make_shared("client", execution_log); + auto invocation_hook = + std::make_shared("invocation", execution_log); + auto provider_hook = + std::make_shared("provider", execution_log); + + ON_CALL(*mock_provider, GetHooks()) + .WillByDefault( + Return(std::vector>{ + provider_hook})); + + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + GlobalHookManager::GetInstance().AddHook(api_hook); + + ClientAPI client(repo_, domain); + client.AddHook(client_hook); + + EvaluationOptions options; + options.hooks = {invocation_hook}; + + bool result = client.GetBooleanValue("test_flag", false, options); + EXPECT_TRUE(result); + + std::vector expected_log = { + "before:api", "before:client", "before:invocation", + "before:provider", "after:provider", "after:invocation", + "after:client", "after:api", "finally:provider", + "finally:invocation", "finally:client", "finally:api", + }; + EXPECT_EQ(execution_log, expected_log); +} + +// Test that evaluation context returned by before hooks accumulates and reaches +// provider +TEST_F(ClientAPITest, + BeforeHookContextMutationPropagatesToSubsequentHooksAndProvider) { + std::string domain = "mutation-domain"; + auto mock_provider = std::make_shared>(); + ON_CALL(*mock_provider, Init(_)).WillByDefault(Return(absl::OkStatus())); + + EvaluationContext captured_context = EvaluationContext::Builder().build(); + EXPECT_CALL(*mock_provider, GetBooleanEvaluation(_, _, _)) + .WillOnce(DoAll( + SaveArg<2>(&captured_context), + Return(std::make_unique( + true, Reason::kTargetingMatch, std::nullopt, FlagMetadata())))); + + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + ClientAPI client(repo_, domain); + client.AddHook(std::make_shared("hook_attr1", "val1")); + client.AddHook(std::make_shared("hook_attr2", "val2")); + + bool result = client.GetBooleanValue("test_flag", false); + EXPECT_TRUE(result); + + ASSERT_NE(captured_context.GetValue("hook_attr1"), nullptr); + EXPECT_EQ( + std::any_cast(*captured_context.GetValue("hook_attr1")), + "val1"); + + ASSERT_NE(captured_context.GetValue("hook_attr2"), nullptr); + EXPECT_EQ( + std::any_cast(*captured_context.GetValue("hook_attr2")), + "val2"); +} + +// Test that HookData is isolated per hook instance and persists across stages +TEST_F(ClientAPITest, HookDataIsIsolatedPerHookAndPersistsAcrossStages) { + std::string domain = "hook-data-domain"; + auto mock_provider = std::make_shared>(); + ON_CALL(*mock_provider, Init(_)).WillByDefault(Return(absl::OkStatus())); + ON_CALL(*mock_provider, GetBooleanEvaluation(_, _, _)) + .WillByDefault(testing::Invoke( + [](std::string_view, bool, const EvaluationContext&) + -> absl::StatusOr> { + return std::make_unique( + true, Reason::kTargetingMatch, std::nullopt, FlagMetadata()); + })); + + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + auto hook_first = std::make_shared("hook-1"); + auto hook_second = std::make_shared("hook-2"); + + ClientAPI client(repo_, domain); + client.AddHooks({hook_first, hook_second}); + + bool result = client.GetBooleanValue("test_flag", false); + EXPECT_TRUE(result); + + EXPECT_EQ(hook_first->after_id_, "hook-1"); + EXPECT_EQ(hook_first->finally_id_, "hook-1"); + + EXPECT_EQ(hook_second->after_id_, "hook-2"); + EXPECT_EQ(hook_second->finally_id_, "hook-2"); +} + +// Test that HookHints are passed to Before, After, and Finally +TEST_F(ClientAPITest, HookHintsArePropagatedToAllStages) { + std::string domain = "hints-domain"; + auto mock_provider = std::make_shared>(); + ON_CALL(*mock_provider, Init(_)).WillByDefault(Return(absl::OkStatus())); + ON_CALL(*mock_provider, GetBooleanEvaluation(_, _, _)) + .WillByDefault(testing::Invoke( + [](std::string_view, bool, const EvaluationContext&) + -> absl::StatusOr> { + return std::make_unique( + true, Reason::kTargetingMatch, std::nullopt, FlagMetadata()); + })); + + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + auto tracking_hook = std::make_shared(); + ClientAPI client(repo_, domain); + client.AddHook(tracking_hook); + + openfeature::HookHints hints; + hints["hint_key"] = std::string("test_hint_value"); + + EvaluationOptions options; + options.hook_hints = hints; + + bool result = client.GetBooleanValue("test_flag", false, options); + EXPECT_TRUE(result); + + EXPECT_EQ(tracking_hook->before_hint_, "test_hint_value"); + EXPECT_EQ(tracking_hook->after_hint_, "test_hint_value"); + EXPECT_EQ(tracking_hook->finally_hint_, "test_hint_value"); +} + +// Test that an error in Before skips resolution and executes Error and Finally +TEST_F(ClientAPITest, + ErrorInBeforeSkipsResolutionAndRunsErrorAndFinallyInReverse) { + std::string domain = "before-error-domain"; + auto mock_provider = std::make_shared>(); + ON_CALL(*mock_provider, Init(_)).WillByDefault(Return(absl::OkStatus())); + EXPECT_CALL(*mock_provider, GetBooleanEvaluation(_, _, _)).Times(0); + + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + std::vector execution_log; + auto throwing_hook = std::make_shared( + ThrowStage::kBefore, "Before hook failed", execution_log); + + ClientAPI client(repo_, domain); + client.AddHook(throwing_hook); + + auto details = client.GetBooleanDetails("test_flag", false); + EXPECT_FALSE(details.GetValue()); + EXPECT_EQ(details.GetReason(), Reason::kError); + EXPECT_EQ(details.GetErrorCode(), ErrorCode::kGeneral); + + std::vector expected_log = {"before", "error", "finally"}; + EXPECT_EQ(execution_log, expected_log); +} + +// Test that an error in After mutates details to error state and executes Error +// and Finally +TEST_F(ClientAPITest, + ErrorInAfterMutatesResultAndRunsErrorAndFinallyInReverse) { + std::string domain = "after-error-domain"; + auto mock_provider = std::make_shared>(); + ON_CALL(*mock_provider, Init(_)).WillByDefault(Return(absl::OkStatus())); + EXPECT_CALL(*mock_provider, GetBooleanEvaluation(_, _, _)) + .WillOnce(Return(std::make_unique( + true, Reason::kTargetingMatch, std::nullopt, FlagMetadata()))); + + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + std::vector execution_log; + auto throwing_hook = std::make_shared( + ThrowStage::kAfter, "After hook failed", execution_log); + + ClientAPI client(repo_, domain); + client.AddHook(throwing_hook); + + auto details = client.GetBooleanDetails("test_flag", false); + EXPECT_FALSE(details.GetValue()); + EXPECT_EQ(details.GetReason(), Reason::kError); + EXPECT_EQ(details.GetErrorCode(), ErrorCode::kGeneral); + + std::vector expected_log = {"before", "after", "error", + "finally"}; + EXPECT_EQ(execution_log, expected_log); +} + +// Test that exceptions thrown inside Error or Finally hooks do not prevent +// other hooks from running +TEST_F(ClientAPITest, ExceptionInErrorOrFinallyDoesNotAbortExecution) { + std::string domain = "fault-tolerance-domain"; + auto mock_provider = std::make_shared>(); + ON_CALL(*mock_provider, Init(_)).WillByDefault(Return(absl::OkStatus())); + + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + std::vector execution_log; + auto failing_hook = std::make_shared( + ThrowStage::kError, "Error hook failed", execution_log); + auto tracking_hook = + std::make_shared("tracker", execution_log); + + // failing_hook throws in Before to trigger Error stage + auto throwing_before_hook = std::make_shared( + ThrowStage::kBefore, "Before failed", execution_log); + + ClientAPI client(repo_, domain); + client.AddHooks({tracking_hook, failing_hook, throwing_before_hook}); + + // Client evaluation MUST NOT throw + EXPECT_NO_THROW({ + auto details = client.GetBooleanDetails("test_flag", false); + EXPECT_FALSE(details.GetValue()); + EXPECT_EQ(details.GetReason(), Reason::kError); + }); +} + +namespace { +class StringTrackingHook : public openfeature::StringHook { + public: + explicit StringTrackingHook(bool& called) : called_(called) {} + std::optional Before( + const openfeature::HookContext& /*ctx*/, + const openfeature::HookHints& /*hints*/) override { + called_ = true; + return std::nullopt; + } + + private: + bool& called_; +}; + +class BoolTrackingHook : public openfeature::BoolHook { + public: + explicit BoolTrackingHook(bool& called) : called_(called) {} + std::optional Before( + const openfeature::HookContext& /*ctx*/, + const openfeature::HookHints& /*hints*/) override { + called_ = true; + return std::nullopt; + } + + private: + bool& called_; +}; +} // namespace + +// Test that type-specific hooks only execute for their matching flag type +TEST_F(ClientAPITest, TypeSpecificHooksExecuteOnlyForMatchingFlagTypes) { + std::string domain = "type-filtering-domain"; + auto mock_provider = std::make_shared>(); + ON_CALL(*mock_provider, Init(_)).WillByDefault(Return(absl::OkStatus())); + + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + bool string_hook_called = false; + bool bool_hook_called = false; + auto string_hook = std::make_shared(string_hook_called); + auto bool_hook = std::make_shared(bool_hook_called); + + ClientAPI client(repo_, domain); + client.AddHooks({string_hook, bool_hook}); + + client.GetBooleanValue("flag_key", false); + EXPECT_TRUE(bool_hook_called); + EXPECT_FALSE(string_hook_called); + + // Now evaluate string flag + bool_hook_called = false; + string_hook_called = false; + client.GetStringValue("string_flag", "default_val"); + EXPECT_TRUE(string_hook_called); + EXPECT_FALSE(bool_hook_called); +} + +// Test that CollectHooks aggregates hooks across all 4 tiers in FIFO order +TEST_F(ClientAPITest, CollectHooksAggregatesAllTiersInPrecedenceOrder) { + std::string domain = "collect-hooks-domain"; + auto mock_provider = std::make_shared>(); + + std::vector execution_log; + auto api_hook = std::make_shared("api", execution_log); + auto client_hook = + std::make_shared("client", execution_log); + auto invocation_hook = + std::make_shared("invocation", execution_log); + auto provider_hook = + std::make_shared("provider", execution_log); + + ON_CALL(*mock_provider, GetHooks()) + .WillByDefault( + Return(std::vector>{ + provider_hook})); + + GlobalHookManager::GetInstance().AddHook(api_hook); + + ClientAPI client(repo_, domain); + client.AddHook(client_hook); + + EvaluationOptions options; + options.hooks = {invocation_hook}; + + auto collected_hooks = + HookSupport::CollectHooks(client.GetHooks(), options, mock_provider); + ASSERT_EQ(collected_hooks.size(), 4); + EXPECT_EQ(collected_hooks[0], api_hook); + EXPECT_EQ(collected_hooks[1], client_hook); + EXPECT_EQ(collected_hooks[2], invocation_hook); + EXPECT_EQ(collected_hooks[3], provider_hook); +} + +// Test that CollectHooks filters out nullptr hooks at all tiers +TEST_F(ClientAPITest, CollectHooksFiltersNullptrsAcrossAllTiers) { + std::string domain = "collect-hooks-nulls-domain"; + auto mock_provider = std::make_shared>(); + + std::vector execution_log; + auto api_hook = std::make_shared("api", execution_log); + auto client_hook = + std::make_shared("client", execution_log); + auto invocation_hook = + std::make_shared("invocation", execution_log); + auto provider_hook = + std::make_shared("provider", execution_log); + + GlobalHookManager::GetInstance().AddHooks({nullptr, api_hook, nullptr}); + + ClientAPI client(repo_, domain); + client.AddHooks({nullptr, client_hook, nullptr}); + + EvaluationOptions options; + options.hooks = {nullptr, invocation_hook, nullptr}; + + ON_CALL(*mock_provider, GetHooks()) + .WillByDefault( + Return(std::vector>{ + nullptr, provider_hook, nullptr})); + + auto collected_hooks = + HookSupport::CollectHooks(client.GetHooks(), options, mock_provider); + ASSERT_EQ(collected_hooks.size(), 4); + EXPECT_EQ(collected_hooks[0], api_hook); + EXPECT_EQ(collected_hooks[1], client_hook); + EXPECT_EQ(collected_hooks[2], invocation_hook); + EXPECT_EQ(collected_hooks[3], provider_hook); +} + +// Test that CollectHooks handles nullopt options and null provider gracefully +TEST_F(ClientAPITest, CollectHooksHandlesNulloptOptionsAndNullProvider) { + std::string domain = "collect-hooks-nullopt-domain"; + + std::vector execution_log; + auto api_hook = std::make_shared("api", execution_log); + auto client_hook = + std::make_shared("client", execution_log); + + GlobalHookManager::GetInstance().AddHook(api_hook); + + ClientAPI client(repo_, domain); + client.AddHook(client_hook); + + auto collected_hooks = + HookSupport::CollectHooks(client.GetHooks(), std::nullopt, nullptr); + ASSERT_EQ(collected_hooks.size(), 2); + EXPECT_EQ(collected_hooks[0], api_hook); + EXPECT_EQ(collected_hooks[1], client_hook); +} + +// Test that CreateHookDataMap creates distinct instances per hook and maps +// duplicates to the same instance +TEST_F(ClientAPITest, + CreateHookDataMapAllocatesUniqueInstancesAndSharesForDuplicates) { + std::vector execution_log; + auto first_hook = std::make_shared("first", execution_log); + auto second_hook = + std::make_shared("second", execution_log); + + auto hook_map = HookSupport::CreateHookDataMap( + {first_hook, second_hook, first_hook, nullptr}); + ASSERT_EQ(hook_map.size(), 2); + ASSERT_NE(hook_map.find(first_hook.get()), hook_map.end()); + ASSERT_NE(hook_map.find(second_hook.get()), hook_map.end()); + + auto first_data = hook_map[first_hook.get()]; + auto second_data = hook_map[second_hook.get()]; + ASSERT_NE(first_data, nullptr); + ASSERT_NE(second_data, nullptr); + EXPECT_NE(first_data, second_data); + + first_data->Set("key", std::string("persisted_value")); + const auto* stored_value = + hook_map[first_hook.get()]->GetAs("key"); + ASSERT_NE(stored_value, nullptr); + EXPECT_EQ(*stored_value, "persisted_value"); +} + +// Test that when provider is in kNotReady status, Error and Finally hooks +// execute with kProviderNotReady +TEST_F(ClientAPITest, ProviderNotReadyTriggersErrorAndFinallyHooks) { + std::string domain = "not-ready-hooks-domain"; + auto mock_provider = std::make_shared>(); + EXPECT_CALL(*mock_provider, GetBooleanEvaluation(_, _, _)).Times(0); + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + auto status_manager = repo_.GetFeatureProviderStatusManager(domain); + ASSERT_NE(status_manager, nullptr); + status_manager->SetStatus(ProviderStatus::kNotReady); + + std::vector execution_log; + auto tracking_hook = + std::make_shared("tracker", execution_log); + + ClientAPI client(repo_, domain); + client.AddHook(tracking_hook); + + auto details = client.GetBooleanDetails("test_flag", false); + EXPECT_FALSE(details.GetValue()); + EXPECT_EQ(details.GetReason(), Reason::kError); + EXPECT_EQ(details.GetErrorCode(), ErrorCode::kProviderNotReady); + + std::vector expected_log = {"before:tracker", "error:tracker", + "finally:tracker"}; + EXPECT_EQ(execution_log, expected_log); +} + +// Test that when provider is in kFatal status, Error and Finally hooks execute +// with kProviderFatal +TEST_F(ClientAPITest, ProviderFatalTriggersErrorAndFinallyHooks) { + std::string domain = "fatal-hooks-domain"; + auto mock_provider = std::make_shared>(); + EXPECT_CALL(*mock_provider, GetBooleanEvaluation(_, _, _)).Times(0); + repo_.SetProvider(domain, mock_provider, EvaluationContext::Builder().build(), + true); + + auto status_manager = repo_.GetFeatureProviderStatusManager(domain); + ASSERT_NE(status_manager, nullptr); + status_manager->SetStatus(ProviderStatus::kFatal); + + std::vector execution_log; + auto tracking_hook = + std::make_shared("tracker", execution_log); + + ClientAPI client(repo_, domain); + client.AddHook(tracking_hook); + + auto details = client.GetBooleanDetails("test_flag", false); + EXPECT_FALSE(details.GetValue()); + EXPECT_EQ(details.GetReason(), Reason::kError); + EXPECT_EQ(details.GetErrorCode(), ErrorCode::kProviderFatal); + + std::vector expected_log = {"before:tracker", "error:tracker", + "finally:tracker"}; + EXPECT_EQ(execution_log, expected_log); +} diff --git a/test/global_hook_manager_test.cpp b/test/global_hook_manager_test.cpp new file mode 100644 index 0000000..ae674ec --- /dev/null +++ b/test/global_hook_manager_test.cpp @@ -0,0 +1,138 @@ +#include "openfeature/global_hook_manager.h" + +#include + +#include +#include +#include +#include + +#include "openfeature/general_hook.h" +#include "openfeature/hook.h" + +namespace openfeature { +namespace { + +class DummyTestHook1 : public BoolHook {}; +class DummyTestHook2 : public StringHook {}; +class DummyTestHook3 : public IntHook {}; + +class GlobalHookManagerTest : public ::testing::Test { + protected: + void SetUp() override { GlobalHookManager::GetInstance().ClearHooks(); } + + void TearDown() override { GlobalHookManager::GetInstance().ClearHooks(); } +}; + +TEST_F(GlobalHookManagerTest, ReturnsSameSingletonInstance) { + GlobalHookManager& instance1 = GlobalHookManager::GetInstance(); + GlobalHookManager& instance2 = GlobalHookManager::GetInstance(); + + EXPECT_EQ(&instance1, &instance2); +} + +TEST_F(GlobalHookManagerTest, InitialStateHasEmptyHooks) { + EXPECT_TRUE(GlobalHookManager::GetInstance().GetHooks().empty()); +} + +TEST_F(GlobalHookManagerTest, AddHookAppendsSingleHook) { + auto& manager = GlobalHookManager::GetInstance(); + std::shared_ptr hook1 = std::make_shared(); + + manager.AddHook(hook1); + + auto hooks = manager.GetHooks(); + ASSERT_EQ(hooks.size(), 1); + EXPECT_EQ(hooks[0], hook1); +} + +TEST_F(GlobalHookManagerTest, AddHooksAppendsMultipleHooksAndPreservesOrder) { + auto& manager = GlobalHookManager::GetInstance(); + std::shared_ptr hook1 = std::make_shared(); + std::shared_ptr hook2 = std::make_shared(); + + manager.AddHooks({hook1, hook2}); + + auto hooks = manager.GetHooks(); + ASSERT_EQ(hooks.size(), 2); + EXPECT_EQ(hooks[0], hook1); + EXPECT_EQ(hooks[1], hook2); + + std::shared_ptr hook3 = std::make_shared(); + manager.AddHook(hook3); + + hooks = manager.GetHooks(); + ASSERT_EQ(hooks.size(), 3); + EXPECT_EQ(hooks[0], hook1); + EXPECT_EQ(hooks[1], hook2); + EXPECT_EQ(hooks[2], hook3); +} + +TEST_F(GlobalHookManagerTest, AddHookAndAddHooksFiltersNullptrs) { + auto& manager = GlobalHookManager::GetInstance(); + manager.AddHook(nullptr); + EXPECT_TRUE(manager.GetHooks().empty()); + + std::shared_ptr valid_hook = std::make_shared(); + manager.AddHooks({nullptr, valid_hook, nullptr}); + + auto hooks = manager.GetHooks(); + ASSERT_EQ(hooks.size(), 1); + EXPECT_EQ(hooks[0], valid_hook); +} + +TEST_F(GlobalHookManagerTest, ClearHooksRemovesAllHooks) { + auto& manager = GlobalHookManager::GetInstance(); + std::shared_ptr hook1 = std::make_shared(); + std::shared_ptr hook2 = std::make_shared(); + manager.AddHooks({hook1, hook2}); + + ASSERT_EQ(manager.GetHooks().size(), 2); + + manager.ClearHooks(); + EXPECT_TRUE(manager.GetHooks().empty()); +} + +constexpr int kWriterSleepIntervalMs = 1; +constexpr int kReaderThreadCount = 8; +constexpr int kStressTestDurationMs = 100; + +TEST_F(GlobalHookManagerTest, ThreadSafetyStressTest) { + auto& manager = GlobalHookManager::GetInstance(); + std::atomic stop{false}; + + // Writer thread adding hooks + std::thread writer([&]() { + while (!stop) { + manager.AddHook(std::make_shared()); + std::this_thread::sleep_for( + std::chrono::milliseconds(kWriterSleepIntervalMs)); + } + }); + + // Reader threads reading hooks + std::vector readers; + readers.reserve(kReaderThreadCount); + for (int i = 0; i < kReaderThreadCount; ++i) { + readers.emplace_back([&]() { + while (!stop) { + auto hooks = manager.GetHooks(); + volatile size_t hook_count = hooks.size(); + (void)hook_count; + } + }); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(kStressTestDurationMs)); + + stop = true; + writer.join(); + for (auto& reader_thread : readers) { + reader_thread.join(); + } + + EXPECT_FALSE(manager.GetHooks().empty()); +} + +} // namespace +} // namespace openfeature diff --git a/test/hook_data_test.cpp b/test/hook_data_test.cpp index 10c7512..fb0603e 100644 --- a/test/hook_data_test.cpp +++ b/test/hook_data_test.cpp @@ -156,4 +156,22 @@ TEST_F(HookDataTest, SetAndGetSharedPtr) { EXPECT_EQ(retrieved->use_count(), kExpectedUseCount); } +TEST_F(HookDataTest, ConstGetAsReturnsPointerToValue) { + hook_data_.Set(kIntKey, kIntValue); + hook_data_.Set(kStringKey, std::string(kStringValue)); + + const HookData& const_hook_data = hook_data_; + + const int* const_int_ptr = const_hook_data.GetAs(kIntKey); + ASSERT_NE(const_int_ptr, nullptr); + EXPECT_EQ(*const_int_ptr, kIntValue); + + const auto* const_str_ptr = const_hook_data.GetAs(kStringKey); + ASSERT_NE(const_str_ptr, nullptr); + EXPECT_EQ(*const_str_ptr, kStringValue); + + EXPECT_EQ(const_hook_data.GetAs(kNonExistentKey), nullptr); + EXPECT_EQ(const_hook_data.GetAs(kIntKey), nullptr); +} + } // namespace openfeature