diff --git a/openfeature/BUILD b/openfeature/BUILD index 1578b39..8c2653c 100644 --- a/openfeature/BUILD +++ b/openfeature/BUILD @@ -49,6 +49,7 @@ cc_library( deps = [ ":evaluation_context", ":features", + ":general_hook", ":metadata", ":provider_status", ], @@ -63,7 +64,9 @@ cc_library( ":client", ":evaluation_context", ":features", + ":flag_evaluation_details", ":flag_metadata", + ":general_hook", ":global_context_manager", ":metadata", ":provider", @@ -118,7 +121,9 @@ cc_library( include_prefix = "openfeature", deps = [ ":evaluation_context", - ":value" + ":evaluation_options", + ":flag_evaluation_details", + ":value", ], ) diff --git a/openfeature/client.h b/openfeature/client.h index c1f2dc0..26f423d 100644 --- a/openfeature/client.h +++ b/openfeature/client.h @@ -1,8 +1,12 @@ #ifndef CPP_SDK_INCLUDE_OPENFEATURE_CLIENT_H_ #define CPP_SDK_INCLUDE_OPENFEATURE_CLIENT_H_ +#include +#include + #include "openfeature/evaluation_context.h" #include "openfeature/features.h" +#include "openfeature/general_hook.h" #include "openfeature/metadata.h" #include "openfeature/provider_status.h" @@ -11,7 +15,7 @@ namespace openfeature { // OpenFeature client implementation. class Client : public Features { public: - virtual ~Client() = default; + ~Client() override = default; virtual Metadata GetMetadata() = 0; // Return an optional client-level evaluation context. @@ -23,7 +27,14 @@ class Client : public Features { // Returns the current status of the associated provider. virtual ProviderStatus GetProviderStatus() = 0; - // TODO: Add methods to add and get Hooks + // Adds one or more hooks to the client-level hook repository. + virtual void AddHooks(std::vector> hooks) = 0; + + // Adds a single hook to the client-level hook repository. + virtual void AddHook(std::shared_ptr hook) = 0; + + // Retrieves all configured client-level hooks. + virtual std::vector> GetHooks() const = 0; }; } // namespace openfeature diff --git a/openfeature/client_api.cpp b/openfeature/client_api.cpp index 58680a4..4071757 100644 --- a/openfeature/client_api.cpp +++ b/openfeature/client_api.cpp @@ -16,12 +16,12 @@ ClientAPI::ClientAPI(ProviderRepository& repository, std::string_view domain) Metadata ClientAPI::GetMetadata() { return Metadata{domain_}; } EvaluationContext ClientAPI::GetEvaluationContext() { - std::lock_guard lock(context_mutex_); + std::scoped_lock lock(context_mutex_); return evaluation_context_; } void ClientAPI::SetEvaluationContext(const EvaluationContext& ctx) { - std::lock_guard lock(context_mutex_); + std::scoped_lock lock(context_mutex_); evaluation_context_ = ctx; } @@ -30,62 +30,283 @@ ProviderStatus ClientAPI::GetProviderStatus() { } bool ClientAPI::GetBooleanValue(std::string_view flag_key, bool default_value) { - return EvaluateBooleanFlag(flag_key, default_value, std::nullopt)->GetValue(); + return EvaluateBooleanFlag(flag_key, default_value, std::nullopt, + std::nullopt) + ->GetValue(); } bool ClientAPI::GetBooleanValue(std::string_view flag_key, bool default_value, const EvaluationContext& ctx) { - return EvaluateBooleanFlag(flag_key, default_value, ctx)->GetValue(); + return EvaluateBooleanFlag(flag_key, default_value, ctx, std::nullopt) + ->GetValue(); +} + +bool ClientAPI::GetBooleanValue(std::string_view flag_key, bool default_value, + const EvaluationOptions& options) { + return EvaluateBooleanFlag(flag_key, default_value, std::nullopt, options) + ->GetValue(); +} + +bool ClientAPI::GetBooleanValue(std::string_view flag_key, bool default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) { + return EvaluateBooleanFlag(flag_key, default_value, ctx, options)->GetValue(); +} + +BoolFlagEvaluationDetails ClientAPI::GetBooleanDetails( + std::string_view flag_key, bool default_value) { + auto res = + EvaluateBooleanFlag(flag_key, default_value, std::nullopt, std::nullopt); + return {std::string(flag_key), *res}; +} + +BoolFlagEvaluationDetails ClientAPI::GetBooleanDetails( + std::string_view flag_key, bool default_value, + const EvaluationContext& ctx) { + auto res = EvaluateBooleanFlag(flag_key, default_value, ctx, std::nullopt); + return {std::string(flag_key), *res}; +} + +BoolFlagEvaluationDetails ClientAPI::GetBooleanDetails( + std::string_view flag_key, bool default_value, + const EvaluationOptions& options) { + auto res = + EvaluateBooleanFlag(flag_key, default_value, std::nullopt, options); + return {std::string(flag_key), *res}; +} + +BoolFlagEvaluationDetails ClientAPI::GetBooleanDetails( + std::string_view flag_key, bool default_value, const EvaluationContext& ctx, + const EvaluationOptions& options) { + auto res = EvaluateBooleanFlag(flag_key, default_value, ctx, options); + return {std::string(flag_key), *res}; } std::string ClientAPI::GetStringValue(std::string_view flag_key, std::string_view default_value) { - return EvaluateStringFlag(flag_key, default_value, std::nullopt)->GetValue(); + return EvaluateStringFlag(flag_key, default_value, std::nullopt, std::nullopt) + ->GetValue(); } std::string ClientAPI::GetStringValue(std::string_view flag_key, std::string_view default_value, const EvaluationContext& ctx) { - return EvaluateStringFlag(flag_key, default_value, ctx)->GetValue(); + return EvaluateStringFlag(flag_key, default_value, ctx, std::nullopt) + ->GetValue(); +} + +std::string ClientAPI::GetStringValue(std::string_view flag_key, + std::string_view default_value, + const EvaluationOptions& options) { + return EvaluateStringFlag(flag_key, default_value, std::nullopt, options) + ->GetValue(); +} + +std::string ClientAPI::GetStringValue(std::string_view flag_key, + std::string_view default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) { + return EvaluateStringFlag(flag_key, default_value, ctx, options)->GetValue(); +} + +StringFlagEvaluationDetails ClientAPI::GetStringDetails( + std::string_view flag_key, std::string_view default_value) { + auto res = + EvaluateStringFlag(flag_key, default_value, std::nullopt, std::nullopt); + return {std::string(flag_key), *res}; +} + +StringFlagEvaluationDetails ClientAPI::GetStringDetails( + std::string_view flag_key, std::string_view default_value, + const EvaluationContext& ctx) { + auto res = EvaluateStringFlag(flag_key, default_value, ctx, std::nullopt); + return {std::string(flag_key), *res}; +} + +StringFlagEvaluationDetails ClientAPI::GetStringDetails( + std::string_view flag_key, std::string_view default_value, + const EvaluationOptions& options) { + auto res = EvaluateStringFlag(flag_key, default_value, std::nullopt, options); + return {std::string(flag_key), *res}; +} + +StringFlagEvaluationDetails ClientAPI::GetStringDetails( + std::string_view flag_key, std::string_view default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) { + auto res = EvaluateStringFlag(flag_key, default_value, ctx, options); + return {std::string(flag_key), *res}; } int64_t ClientAPI::GetIntegerValue(std::string_view flag_key, int64_t default_value) { - return EvaluateIntegerFlag(flag_key, default_value, std::nullopt)->GetValue(); + return EvaluateIntegerFlag(flag_key, default_value, std::nullopt, + std::nullopt) + ->GetValue(); } int64_t ClientAPI::GetIntegerValue(std::string_view flag_key, int64_t default_value, const EvaluationContext& ctx) { - return EvaluateIntegerFlag(flag_key, default_value, ctx)->GetValue(); + return EvaluateIntegerFlag(flag_key, default_value, ctx, std::nullopt) + ->GetValue(); +} + +int64_t ClientAPI::GetIntegerValue(std::string_view flag_key, + int64_t default_value, + const EvaluationOptions& options) { + return EvaluateIntegerFlag(flag_key, default_value, std::nullopt, options) + ->GetValue(); +} + +int64_t ClientAPI::GetIntegerValue(std::string_view flag_key, + int64_t default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) { + return EvaluateIntegerFlag(flag_key, default_value, ctx, options)->GetValue(); +} + +IntFlagEvaluationDetails ClientAPI::GetIntegerDetails(std::string_view flag_key, + int64_t default_value) { + auto res = + EvaluateIntegerFlag(flag_key, default_value, std::nullopt, std::nullopt); + return {std::string(flag_key), *res}; +} + +IntFlagEvaluationDetails ClientAPI::GetIntegerDetails( + std::string_view flag_key, int64_t default_value, + const EvaluationContext& ctx) { + auto res = EvaluateIntegerFlag(flag_key, default_value, ctx, std::nullopt); + return {std::string(flag_key), *res}; +} + +IntFlagEvaluationDetails ClientAPI::GetIntegerDetails( + std::string_view flag_key, int64_t default_value, + const EvaluationOptions& options) { + auto res = + EvaluateIntegerFlag(flag_key, default_value, std::nullopt, options); + return {std::string(flag_key), *res}; +} + +IntFlagEvaluationDetails ClientAPI::GetIntegerDetails( + std::string_view flag_key, int64_t default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) { + auto res = EvaluateIntegerFlag(flag_key, default_value, ctx, options); + return {std::string(flag_key), *res}; } double ClientAPI::GetDoubleValue(std::string_view flag_key, double default_value) { - return EvaluateDoubleFlag(flag_key, default_value, std::nullopt)->GetValue(); + return EvaluateDoubleFlag(flag_key, default_value, std::nullopt, std::nullopt) + ->GetValue(); } double ClientAPI::GetDoubleValue(std::string_view flag_key, double default_value, const EvaluationContext& ctx) { - return EvaluateDoubleFlag(flag_key, default_value, ctx)->GetValue(); + return EvaluateDoubleFlag(flag_key, default_value, ctx, std::nullopt) + ->GetValue(); +} + +double ClientAPI::GetDoubleValue(std::string_view flag_key, + double default_value, + const EvaluationOptions& options) { + return EvaluateDoubleFlag(flag_key, default_value, std::nullopt, options) + ->GetValue(); +} + +double ClientAPI::GetDoubleValue(std::string_view flag_key, + double default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) { + return EvaluateDoubleFlag(flag_key, default_value, ctx, options)->GetValue(); +} + +DoubleFlagEvaluationDetails ClientAPI::GetDoubleDetails( + std::string_view flag_key, double default_value) { + auto res = + EvaluateDoubleFlag(flag_key, default_value, std::nullopt, std::nullopt); + return {std::string(flag_key), *res}; +} + +DoubleFlagEvaluationDetails ClientAPI::GetDoubleDetails( + std::string_view flag_key, double default_value, + const EvaluationContext& ctx) { + auto res = EvaluateDoubleFlag(flag_key, default_value, ctx, std::nullopt); + return {std::string(flag_key), *res}; +} + +DoubleFlagEvaluationDetails ClientAPI::GetDoubleDetails( + std::string_view flag_key, double default_value, + const EvaluationOptions& options) { + auto res = EvaluateDoubleFlag(flag_key, default_value, std::nullopt, options); + return {std::string(flag_key), *res}; +} + +DoubleFlagEvaluationDetails ClientAPI::GetDoubleDetails( + std::string_view flag_key, double default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) { + auto res = EvaluateDoubleFlag(flag_key, default_value, ctx, options); + return {std::string(flag_key), *res}; } Value ClientAPI::GetObjectValue(std::string_view flag_key, Value default_value) { - return EvaluateObjectFlag(flag_key, default_value, std::nullopt)->GetValue(); + return EvaluateObjectFlag(flag_key, default_value, std::nullopt, std::nullopt) + ->GetValue(); } Value ClientAPI::GetObjectValue(std::string_view flag_key, Value default_value, const EvaluationContext& ctx) { - return EvaluateObjectFlag(flag_key, default_value, ctx)->GetValue(); + return EvaluateObjectFlag(flag_key, default_value, ctx, std::nullopt) + ->GetValue(); +} + +Value ClientAPI::GetObjectValue(std::string_view flag_key, Value default_value, + const EvaluationOptions& options) { + return EvaluateObjectFlag(flag_key, default_value, std::nullopt, options) + ->GetValue(); +} + +Value ClientAPI::GetObjectValue(std::string_view flag_key, Value default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) { + return EvaluateObjectFlag(flag_key, default_value, ctx, options)->GetValue(); +} + +ObjectFlagEvaluationDetails ClientAPI::GetObjectDetails( + std::string_view flag_key, Value default_value) { + auto res = + EvaluateObjectFlag(flag_key, default_value, std::nullopt, std::nullopt); + return {std::string(flag_key), *res}; +} + +ObjectFlagEvaluationDetails ClientAPI::GetObjectDetails( + std::string_view flag_key, Value default_value, + const EvaluationContext& ctx) { + auto res = EvaluateObjectFlag(flag_key, default_value, ctx, std::nullopt); + return {std::string(flag_key), *res}; +} + +ObjectFlagEvaluationDetails ClientAPI::GetObjectDetails( + std::string_view flag_key, Value default_value, + const EvaluationOptions& options) { + auto res = EvaluateObjectFlag(flag_key, default_value, std::nullopt, options); + return {std::string(flag_key), *res}; +} + +ObjectFlagEvaluationDetails ClientAPI::GetObjectDetails( + std::string_view flag_key, Value default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) { + auto res = EvaluateObjectFlag(flag_key, default_value, ctx, options); + return {std::string(flag_key), *res}; } std::unique_ptr ClientAPI::EvaluateBooleanFlag( std::string_view flag_key, bool default_value, - const std::optional& ctx) { + const std::optional& ctx, + const std::optional& options) { return this->EvaluateFlag( - default_value, ctx, + default_value, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetBooleanEvaluation(flag_key, default_value, @@ -95,10 +316,11 @@ std::unique_ptr ClientAPI::EvaluateBooleanFlag( std::unique_ptr ClientAPI::EvaluateStringFlag( std::string_view flag_key, std::string_view default_value, - const std::optional& ctx) { + const std::optional& ctx, + const std::optional& options) { std::string default_str(default_value); return this->EvaluateFlag( - default_str, ctx, + default_str, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetStringEvaluation(flag_key, default_value, @@ -108,9 +330,10 @@ std::unique_ptr ClientAPI::EvaluateStringFlag( std::unique_ptr ClientAPI::EvaluateIntegerFlag( std::string_view flag_key, int64_t default_value, - const std::optional& ctx) { + const std::optional& ctx, + const std::optional& options) { return this->EvaluateFlag( - default_value, ctx, + default_value, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetIntegerEvaluation(flag_key, default_value, @@ -120,9 +343,10 @@ std::unique_ptr ClientAPI::EvaluateIntegerFlag( std::unique_ptr ClientAPI::EvaluateDoubleFlag( std::string_view flag_key, double default_value, - const std::optional& ctx) { + const std::optional& ctx, + const std::optional& options) { return this->EvaluateFlag( - default_value, ctx, + default_value, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetDoubleEvaluation(flag_key, default_value, @@ -132,9 +356,10 @@ std::unique_ptr ClientAPI::EvaluateDoubleFlag( std::unique_ptr ClientAPI::EvaluateObjectFlag( std::string_view flag_key, Value default_value, - const std::optional& ctx) { + const std::optional& ctx, + const std::optional& options) { return this->EvaluateFlag( - default_value, ctx, + default_value, ctx, options, [&](const std::shared_ptr& provider, const EvaluationContext& merged_ctx) { return provider->GetObjectEvaluation(flag_key, default_value, @@ -151,9 +376,29 @@ EvaluationContext ClientAPI::MergeContexts( if (invocation_ctx.has_value()) { return EvaluationContext::Merge( {&global_ctx, &client_ctx, &(*invocation_ctx)}); - } else { - return EvaluationContext::Merge({&global_ctx, &client_ctx}); } + return EvaluationContext::Merge({&global_ctx, &client_ctx}); +} + +void ClientAPI::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 ClientAPI::AddHook(std::shared_ptr hook) { + if (hook == nullptr) return; + std::unique_lock lock(hooks_mutex_); + hooks_.push_back(std::move(hook)); +} + +std::vector> ClientAPI::GetHooks() const { + std::shared_lock lock(hooks_mutex_); + return hooks_; } } // namespace openfeature diff --git a/openfeature/client_api.h b/openfeature/client_api.h index d55e424..dec0ad3 100644 --- a/openfeature/client_api.h +++ b/openfeature/client_api.h @@ -6,10 +6,13 @@ #include #include #include +#include #include "openfeature/client.h" #include "openfeature/evaluation_context.h" #include "openfeature/features.h" +#include "openfeature/flag_evaluation_details.h" +#include "openfeature/general_hook.h" #include "openfeature/global_context_manager.h" #include "openfeature/metadata.h" #include "openfeature/provider.h" @@ -45,6 +48,24 @@ class ClientAPI : public Client { bool GetBooleanValue(std::string_view flag_key, bool default_value) override; bool GetBooleanValue(std::string_view flag_key, bool default_value, const EvaluationContext& ctx) override; + bool GetBooleanValue(std::string_view flag_key, bool default_value, + const EvaluationOptions& options) override; + bool GetBooleanValue(std::string_view flag_key, bool default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) override; + + // Detailed boolean flag evaluation. + BoolFlagEvaluationDetails GetBooleanDetails(std::string_view flag_key, + bool default_value) override; + BoolFlagEvaluationDetails GetBooleanDetails( + std::string_view flag_key, bool default_value, + const EvaluationContext& ctx) override; + BoolFlagEvaluationDetails GetBooleanDetails( + std::string_view flag_key, bool default_value, + const EvaluationOptions& options) override; + BoolFlagEvaluationDetails GetBooleanDetails( + std::string_view flag_key, bool default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) override; // Evaluate a string flag. std::string GetStringValue(std::string_view flag_key, @@ -52,52 +73,139 @@ class ClientAPI : public Client { std::string GetStringValue(std::string_view flag_key, std::string_view default_value, const EvaluationContext& ctx) override; + std::string GetStringValue(std::string_view flag_key, + std::string_view default_value, + const EvaluationOptions& options) override; + std::string GetStringValue(std::string_view flag_key, + std::string_view default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) override; + + // Detailed string flag evaluation. + StringFlagEvaluationDetails GetStringDetails( + std::string_view flag_key, std::string_view default_value) override; + StringFlagEvaluationDetails GetStringDetails( + std::string_view flag_key, std::string_view default_value, + const EvaluationContext& ctx) override; + StringFlagEvaluationDetails GetStringDetails( + std::string_view flag_key, std::string_view default_value, + const EvaluationOptions& options) override; + StringFlagEvaluationDetails GetStringDetails( + std::string_view flag_key, std::string_view default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) override; // Evaluate an integer flag. int64_t GetIntegerValue(std::string_view flag_key, int64_t default_value) override; int64_t GetIntegerValue(std::string_view flag_key, int64_t default_value, const EvaluationContext& ctx) override; + int64_t GetIntegerValue(std::string_view flag_key, int64_t default_value, + const EvaluationOptions& options) override; + int64_t GetIntegerValue(std::string_view flag_key, int64_t default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) override; + + // Detailed integer flag evaluation. + IntFlagEvaluationDetails GetIntegerDetails(std::string_view flag_key, + int64_t default_value) override; + IntFlagEvaluationDetails GetIntegerDetails( + std::string_view flag_key, int64_t default_value, + const EvaluationContext& ctx) override; + IntFlagEvaluationDetails GetIntegerDetails( + std::string_view flag_key, int64_t default_value, + const EvaluationOptions& options) override; + IntFlagEvaluationDetails GetIntegerDetails( + std::string_view flag_key, int64_t default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) override; + // Evaluate a double flag. double GetDoubleValue(std::string_view flag_key, double default_value) override; double GetDoubleValue(std::string_view flag_key, double default_value, const EvaluationContext& ctx) override; + double GetDoubleValue(std::string_view flag_key, double default_value, + const EvaluationOptions& options) override; + double GetDoubleValue(std::string_view flag_key, double default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) override; + + // Detailed double flag evaluation. + DoubleFlagEvaluationDetails GetDoubleDetails(std::string_view flag_key, + double default_value) override; + DoubleFlagEvaluationDetails GetDoubleDetails( + std::string_view flag_key, double default_value, + const EvaluationContext& ctx) override; + DoubleFlagEvaluationDetails GetDoubleDetails( + std::string_view flag_key, double default_value, + const EvaluationOptions& options) override; + DoubleFlagEvaluationDetails GetDoubleDetails( + std::string_view flag_key, double default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) override; + // Evaluate an object flag. Value GetObjectValue(std::string_view flag_key, Value default_value) override; Value GetObjectValue(std::string_view flag_key, Value default_value, const EvaluationContext& ctx) override; + Value GetObjectValue(std::string_view flag_key, Value default_value, + const EvaluationOptions& options) override; + Value GetObjectValue(std::string_view flag_key, Value default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) override; + + // Detailed object flag evaluation. + ObjectFlagEvaluationDetails GetObjectDetails(std::string_view flag_key, + Value default_value) override; + ObjectFlagEvaluationDetails GetObjectDetails( + std::string_view flag_key, Value default_value, + const EvaluationContext& ctx) override; + ObjectFlagEvaluationDetails GetObjectDetails( + std::string_view flag_key, Value default_value, + const EvaluationOptions& options) override; + ObjectFlagEvaluationDetails GetObjectDetails( + std::string_view flag_key, Value default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) override; + + // Adds one or more hooks to the client-level hook repository. + void AddHooks(std::vector> hooks) override; + + // Adds a single hook to the client-level hook repository. + void AddHook(std::shared_ptr hook) override; - // TODO: Add methods to get and set Hooks. - // TODO: Add methods for detailed flag evaluation. - // TODO: Overload method "GetBooleanValue" to accept "Evaluation Options". + // Retrieves all configured client-level hooks. + std::vector> GetHooks() const override; private: template std::unique_ptr EvaluateFlag( ValueType default_value, const std::optional& ctx, + const std::optional& options, ProviderCallable provider_call); std::unique_ptr EvaluateBooleanFlag( std::string_view flag_key, bool default_value, - const std::optional& ctx); + const std::optional& ctx, + const std::optional& options = std::nullopt); std::unique_ptr EvaluateStringFlag( std::string_view flag_key, std::string_view default_value, - const std::optional& ctx); + const std::optional& ctx, + const std::optional& options = std::nullopt); std::unique_ptr EvaluateIntegerFlag( std::string_view flag_key, int64_t default_value, - const std::optional& ctx); + const std::optional& ctx, + const std::optional& options = std::nullopt); std::unique_ptr EvaluateDoubleFlag( std::string_view flag_key, double default_value, - const std::optional& ctx); + const std::optional& ctx, + const std::optional& options = std::nullopt); std::unique_ptr EvaluateObjectFlag( std::string_view flag_key, Value default_value, - const std::optional& ctx); + const std::optional& ctx, + const std::optional& options = std::nullopt); EvaluationContext MergeContexts( const std::optional& invocation_ctx); @@ -106,12 +214,15 @@ class ClientAPI : public Client { std::string domain_; EvaluationContext evaluation_context_; mutable std::mutex context_mutex_; + mutable std::shared_mutex hooks_mutex_; + std::vector> hooks_; }; template std::unique_ptr ClientAPI::EvaluateFlag( ValueType default_value, const std::optional& ctx, + const std::optional& options, ProviderCallable provider_call) { std::shared_ptr manager = provider_repository_.GetFeatureProviderStatusManager(domain_); diff --git a/openfeature/features.h b/openfeature/features.h index 83897eb..d553a01 100644 --- a/openfeature/features.h +++ b/openfeature/features.h @@ -4,6 +4,8 @@ #include #include "openfeature/evaluation_context.h" +#include "openfeature/evaluation_options.h" +#include "openfeature/flag_evaluation_details.h" #include "openfeature/value.h" namespace openfeature { @@ -16,30 +18,112 @@ class Features { bool default_value) = 0; virtual bool GetBooleanValue(std::string_view flag_key, bool default_value, const EvaluationContext& ctx) = 0; + virtual bool GetBooleanValue(std::string_view flag_key, bool default_value, + const EvaluationOptions& options) = 0; + virtual bool GetBooleanValue(std::string_view flag_key, bool default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) = 0; + virtual BoolFlagEvaluationDetails GetBooleanDetails(std::string_view flag_key, + bool default_value) = 0; + virtual BoolFlagEvaluationDetails GetBooleanDetails( + std::string_view flag_key, bool default_value, + const EvaluationContext& ctx) = 0; + virtual BoolFlagEvaluationDetails GetBooleanDetails( + std::string_view flag_key, bool default_value, + const EvaluationOptions& options) = 0; + virtual BoolFlagEvaluationDetails GetBooleanDetails( + std::string_view flag_key, bool default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) = 0; virtual std::string GetStringValue(std::string_view flag_key, std::string_view default_value) = 0; virtual std::string GetStringValue(std::string_view flag_key, std::string_view default_value, const EvaluationContext& ctx) = 0; + virtual std::string GetStringValue(std::string_view flag_key, + std::string_view default_value, + const EvaluationOptions& options) = 0; + virtual std::string GetStringValue(std::string_view flag_key, + std::string_view default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) = 0; + virtual StringFlagEvaluationDetails GetStringDetails( + std::string_view flag_key, std::string_view default_value) = 0; + virtual StringFlagEvaluationDetails GetStringDetails( + std::string_view flag_key, std::string_view default_value, + const EvaluationContext& ctx) = 0; + virtual StringFlagEvaluationDetails GetStringDetails( + std::string_view flag_key, std::string_view default_value, + const EvaluationOptions& options) = 0; + virtual StringFlagEvaluationDetails GetStringDetails( + std::string_view flag_key, std::string_view default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) = 0; virtual int64_t GetIntegerValue(std::string_view flag_key, int64_t default_value) = 0; virtual int64_t GetIntegerValue(std::string_view flag_key, int64_t default_value, const EvaluationContext& ctx) = 0; + virtual int64_t GetIntegerValue(std::string_view flag_key, + int64_t default_value, + const EvaluationOptions& options) = 0; + virtual int64_t GetIntegerValue(std::string_view flag_key, + int64_t default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) = 0; + virtual IntFlagEvaluationDetails GetIntegerDetails(std::string_view flag_key, + int64_t default_value) = 0; + virtual IntFlagEvaluationDetails GetIntegerDetails( + std::string_view flag_key, int64_t default_value, + const EvaluationContext& ctx) = 0; + virtual IntFlagEvaluationDetails GetIntegerDetails( + std::string_view flag_key, int64_t default_value, + const EvaluationOptions& options) = 0; + virtual IntFlagEvaluationDetails GetIntegerDetails( + std::string_view flag_key, int64_t default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) = 0; virtual double GetDoubleValue(std::string_view flag_key, double default_value) = 0; virtual double GetDoubleValue(std::string_view flag_key, double default_value, const EvaluationContext& ctx) = 0; + virtual double GetDoubleValue(std::string_view flag_key, double default_value, + const EvaluationOptions& options) = 0; + virtual double GetDoubleValue(std::string_view flag_key, double default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) = 0; + virtual DoubleFlagEvaluationDetails GetDoubleDetails( + std::string_view flag_key, double default_value) = 0; + virtual DoubleFlagEvaluationDetails GetDoubleDetails( + std::string_view flag_key, double default_value, + const EvaluationContext& ctx) = 0; + virtual DoubleFlagEvaluationDetails GetDoubleDetails( + std::string_view flag_key, double default_value, + const EvaluationOptions& options) = 0; + virtual DoubleFlagEvaluationDetails GetDoubleDetails( + std::string_view flag_key, double default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) = 0; virtual Value GetObjectValue(std::string_view flag_key, Value default_value) = 0; virtual Value GetObjectValue(std::string_view flag_key, Value default_value, const EvaluationContext& ctx) = 0; - - // TODO: Add detailed evaluation methods + virtual Value GetObjectValue(std::string_view flag_key, Value default_value, + const EvaluationOptions& options) = 0; + virtual Value GetObjectValue(std::string_view flag_key, Value default_value, + const EvaluationContext& ctx, + const EvaluationOptions& options) = 0; + virtual ObjectFlagEvaluationDetails GetObjectDetails( + std::string_view flag_key, Value default_value) = 0; + virtual ObjectFlagEvaluationDetails GetObjectDetails( + std::string_view flag_key, Value default_value, + const EvaluationContext& ctx) = 0; + virtual ObjectFlagEvaluationDetails GetObjectDetails( + std::string_view flag_key, Value default_value, + const EvaluationOptions& options) = 0; + virtual ObjectFlagEvaluationDetails GetObjectDetails( + std::string_view flag_key, Value default_value, + const EvaluationContext& ctx, const EvaluationOptions& options) = 0; }; } // namespace openfeature diff --git a/openfeature/openfeature_api.h b/openfeature/openfeature_api.h index a9fb4b9..d2a8fc6 100644 --- a/openfeature/openfeature_api.h +++ b/openfeature/openfeature_api.h @@ -4,6 +4,7 @@ #include #include #include +#pragma endregion #include "openfeature/client.h" #include "openfeature/evaluation_context.h" diff --git a/test/BUILD b/test/BUILD index 799e0bb..70ba6f5 100644 --- a/test/BUILD +++ b/test/BUILD @@ -20,6 +20,8 @@ cc_test( deps = [ ":mock_feature_provider", "//openfeature:client_api", + "//openfeature:flag_evaluation_details", + "//openfeature:hook", "@googletest//:gtest_main", ], ) diff --git a/test/client_api_test.cpp b/test/client_api_test.cpp index 5008f36..28acf18 100644 --- a/test/client_api_test.cpp +++ b/test/client_api_test.cpp @@ -13,20 +13,28 @@ #include "absl/status/status.h" #include "mocks/mock_feature_provider.h" #include "openfeature/evaluation_context.h" +#include "openfeature/evaluation_options.h" #include "openfeature/global_context_manager.h" +#include "openfeature/hook.h" #include "openfeature/provider_status.h" +using ::openfeature::BoolFlagEvaluationDetails; using ::openfeature::BoolResolutionDetails; using ::openfeature::ClientAPI; +using ::openfeature::DoubleFlagEvaluationDetails; using ::openfeature::ErrorCode; using ::openfeature::EvaluationContext; +using ::openfeature::EvaluationOptions; using ::openfeature::FlagMetadata; using ::openfeature::GlobalContextManager; +using ::openfeature::IntFlagEvaluationDetails; using ::openfeature::Metadata; using ::openfeature::MockFeatureProvider; +using ::openfeature::ObjectFlagEvaluationDetails; using ::openfeature::ProviderRepository; using ::openfeature::ProviderStatus; using ::openfeature::Reason; +using ::openfeature::StringFlagEvaluationDetails; using ::openfeature::Value; using ::testing::_; using ::testing::DoAll; @@ -47,6 +55,9 @@ class ClientAPITest : public ::testing::Test { constexpr int kUnknownExceptionError = 43; constexpr int kSleepTimeMs = 10; +constexpr int64_t kDefaultIntValue = 42; +constexpr double kDefaultDoubleValue = 3.14; +constexpr int kDefaultObjectIntValue = 100; // Test that the constructor correctly sets the domain in the metadata. TEST_F(ClientAPITest, ConstructorSetsDomainMetadata) { @@ -96,7 +107,8 @@ TEST_F(ClientAPITest, GetStringValueReturnsDefaultWithNoopProvider) { TEST_F(ClientAPITest, GetIntegerValueReturnsDefaultWithNoopProvider) { ClientAPI client(repo_, "test-domain"); std::string flag_key = "my-integer-flag"; - EXPECT_EQ(client.GetIntegerValue(flag_key, 42), 42); + EXPECT_EQ(client.GetIntegerValue(flag_key, kDefaultIntValue), + kDefaultIntValue); } // Test that GetDoubleValue returns the default value when using the default @@ -104,7 +116,8 @@ TEST_F(ClientAPITest, GetIntegerValueReturnsDefaultWithNoopProvider) { TEST_F(ClientAPITest, GetDoubleValueReturnsDefaultWithNoopProvider) { ClientAPI client(repo_, "test-domain"); std::string flag_key = "my-double-flag"; - EXPECT_DOUBLE_EQ(client.GetDoubleValue(flag_key, 3.14), 3.14); + EXPECT_DOUBLE_EQ(client.GetDoubleValue(flag_key, kDefaultDoubleValue), + kDefaultDoubleValue); } // Test that GetObjectValue returns the default value when using the default @@ -138,7 +151,8 @@ TEST_F(ClientAPITest, GetIntegerValueWithContextReturnsDefault) { ClientAPI client(repo_, "test-domain"); EvaluationContext ctx = EvaluationContext::Builder().build(); std::string flag_key = "my-integer-flag"; - EXPECT_EQ(client.GetIntegerValue(flag_key, 42, ctx), 42); + EXPECT_EQ(client.GetIntegerValue(flag_key, kDefaultIntValue, ctx), + kDefaultIntValue); } // Test that GetDoubleValue with an EvaluationContext passed in. @@ -146,7 +160,8 @@ TEST_F(ClientAPITest, GetDoubleValueWithContextReturnsDefault) { ClientAPI client(repo_, "test-domain"); EvaluationContext ctx = EvaluationContext::Builder().build(); std::string flag_key = "my-double-flag"; - EXPECT_DOUBLE_EQ(client.GetDoubleValue(flag_key, 3.14, ctx), 3.14); + EXPECT_DOUBLE_EQ(client.GetDoubleValue(flag_key, kDefaultDoubleValue, ctx), + kDefaultDoubleValue); } // Test that GetObjectValue with an EvaluationContext passed in. @@ -157,6 +172,216 @@ TEST_F(ClientAPITest, GetObjectValueWithContextReturnsDefault) { EXPECT_EQ(client.GetObjectValue(flag_key, Value(1), ctx), Value(1)); } +// Test GetBooleanValue with EvaluationOptions. +TEST_F(ClientAPITest, GetBooleanValueWithOptionsReturnsDefault) { + ClientAPI client(repo_, "test-domain"); + EvaluationOptions options; + std::string flag_key = "my-boolean-flag"; + + EXPECT_TRUE(client.GetBooleanValue(flag_key, true, options)); + EXPECT_FALSE(client.GetBooleanValue(flag_key, false, options)); + + EvaluationContext ctx = EvaluationContext::Builder().build(); + EXPECT_TRUE(client.GetBooleanValue(flag_key, true, ctx, options)); + EXPECT_FALSE(client.GetBooleanValue(flag_key, false, ctx, options)); +} + +// Test GetStringValue with EvaluationOptions. +TEST_F(ClientAPITest, GetStringValueWithOptionsReturnsDefault) { + ClientAPI client(repo_, "test-domain"); + EvaluationOptions options; + std::string flag_key = "my-string-flag"; + + EXPECT_EQ(client.GetStringValue(flag_key, "default", options), "default"); + + EvaluationContext ctx = EvaluationContext::Builder().build(); + EXPECT_EQ(client.GetStringValue(flag_key, "default", ctx, options), + "default"); +} + +// Test GetIntegerValue with EvaluationOptions. +TEST_F(ClientAPITest, GetIntegerValueWithOptionsReturnsDefault) { + ClientAPI client(repo_, "test-domain"); + EvaluationOptions options; + std::string flag_key = "my-integer-flag"; + + EXPECT_EQ(client.GetIntegerValue(flag_key, kDefaultIntValue, options), + kDefaultIntValue); + + EvaluationContext ctx = EvaluationContext::Builder().build(); + EXPECT_EQ(client.GetIntegerValue(flag_key, kDefaultIntValue, ctx, options), + kDefaultIntValue); +} + +// Test GetDoubleValue with EvaluationOptions. +TEST_F(ClientAPITest, GetDoubleValueWithOptionsReturnsDefault) { + ClientAPI client(repo_, "test-domain"); + EvaluationOptions options; + std::string flag_key = "my-double-flag"; + + EXPECT_DOUBLE_EQ( + client.GetDoubleValue(flag_key, kDefaultDoubleValue, options), + kDefaultDoubleValue); + + EvaluationContext ctx = EvaluationContext::Builder().build(); + EXPECT_DOUBLE_EQ( + client.GetDoubleValue(flag_key, kDefaultDoubleValue, ctx, options), + kDefaultDoubleValue); +} + +// Test GetObjectValue with EvaluationOptions. +TEST_F(ClientAPITest, GetObjectValueWithOptionsReturnsDefault) { + ClientAPI client(repo_, "test-domain"); + EvaluationOptions options; + std::string flag_key = "my-object-flag"; + + EXPECT_EQ(client.GetObjectValue(flag_key, Value(1), options), Value(1)); + + EvaluationContext ctx = EvaluationContext::Builder().build(); + EXPECT_EQ(client.GetObjectValue(flag_key, Value(1), ctx, options), Value(1)); +} + +// Test GetBooleanDetails returns proper details with NoopProvider. +TEST_F(ClientAPITest, GetBooleanDetailsReturnsDetailsWithNoopProvider) { + ClientAPI client(repo_, "test-domain"); + std::string flag_key = "my-boolean-flag"; + EvaluationContext ctx = EvaluationContext::Builder().build(); + EvaluationOptions options; + + auto details1 = client.GetBooleanDetails(flag_key, true); + EXPECT_EQ(details1.GetFlagKey(), flag_key); + EXPECT_TRUE(details1.GetValue()); + EXPECT_EQ(details1.GetReason(), Reason::kDefault); + EXPECT_EQ(details1.GetErrorCode(), std::nullopt); + EXPECT_EQ(details1.GetErrorMessage(), ""); + EXPECT_EQ(details1.GetVariant(), "default-variant"); + + auto details2 = client.GetBooleanDetails(flag_key, false, ctx); + EXPECT_EQ(details2.GetFlagKey(), flag_key); + EXPECT_FALSE(details2.GetValue()); + EXPECT_EQ(details2.GetReason(), Reason::kDefault); + + auto details3 = client.GetBooleanDetails(flag_key, true, options); + EXPECT_EQ(details3.GetFlagKey(), flag_key); + EXPECT_TRUE(details3.GetValue()); + + auto details4 = client.GetBooleanDetails(flag_key, false, ctx, options); + EXPECT_EQ(details4.GetFlagKey(), flag_key); + EXPECT_FALSE(details4.GetValue()); +} + +// Test GetStringDetails returns proper details with NoopProvider. +TEST_F(ClientAPITest, GetStringDetailsReturnsDetailsWithNoopProvider) { + ClientAPI client(repo_, "test-domain"); + std::string flag_key = "my-string-flag"; + EvaluationContext ctx = EvaluationContext::Builder().build(); + EvaluationOptions options; + + auto details1 = client.GetStringDetails(flag_key, "default_val"); + EXPECT_EQ(details1.GetFlagKey(), flag_key); + EXPECT_EQ(details1.GetValue(), "default_val"); + EXPECT_EQ(details1.GetReason(), Reason::kDefault); + EXPECT_EQ(details1.GetErrorCode(), std::nullopt); + + auto details2 = client.GetStringDetails(flag_key, "default_val", ctx); + EXPECT_EQ(details2.GetFlagKey(), flag_key); + EXPECT_EQ(details2.GetValue(), "default_val"); + + auto details3 = client.GetStringDetails(flag_key, "default_val", options); + EXPECT_EQ(details3.GetFlagKey(), flag_key); + EXPECT_EQ(details3.GetValue(), "default_val"); + + auto details4 = + client.GetStringDetails(flag_key, "default_val", ctx, options); + EXPECT_EQ(details4.GetFlagKey(), flag_key); + EXPECT_EQ(details4.GetValue(), "default_val"); +} + +// Test GetIntegerDetails returns proper details with NoopProvider. +TEST_F(ClientAPITest, GetIntegerDetailsReturnsDetailsWithNoopProvider) { + ClientAPI client(repo_, "test-domain"); + std::string flag_key = "my-integer-flag"; + EvaluationContext ctx = EvaluationContext::Builder().build(); + EvaluationOptions options; + + auto details1 = client.GetIntegerDetails(flag_key, kDefaultIntValue); + EXPECT_EQ(details1.GetFlagKey(), flag_key); + EXPECT_EQ(details1.GetValue(), kDefaultIntValue); + EXPECT_EQ(details1.GetReason(), Reason::kDefault); + EXPECT_EQ(details1.GetErrorCode(), std::nullopt); + + auto details2 = client.GetIntegerDetails(flag_key, kDefaultIntValue, ctx); + EXPECT_EQ(details2.GetFlagKey(), flag_key); + EXPECT_EQ(details2.GetValue(), kDefaultIntValue); + + auto details3 = client.GetIntegerDetails(flag_key, kDefaultIntValue, options); + EXPECT_EQ(details3.GetFlagKey(), flag_key); + EXPECT_EQ(details3.GetValue(), kDefaultIntValue); + + auto details4 = + client.GetIntegerDetails(flag_key, kDefaultIntValue, ctx, options); + EXPECT_EQ(details4.GetFlagKey(), flag_key); + EXPECT_EQ(details4.GetValue(), kDefaultIntValue); +} + +// Test GetDoubleDetails returns proper details with NoopProvider. +TEST_F(ClientAPITest, GetDoubleDetailsReturnsDetailsWithNoopProvider) { + ClientAPI client(repo_, "test-domain"); + std::string flag_key = "my-double-flag"; + EvaluationContext ctx = EvaluationContext::Builder().build(); + EvaluationOptions options; + + auto details1 = client.GetDoubleDetails(flag_key, kDefaultDoubleValue); + EXPECT_EQ(details1.GetFlagKey(), flag_key); + EXPECT_DOUBLE_EQ(details1.GetValue(), kDefaultDoubleValue); + EXPECT_EQ(details1.GetReason(), Reason::kDefault); + EXPECT_EQ(details1.GetErrorCode(), std::nullopt); + + auto details2 = client.GetDoubleDetails(flag_key, kDefaultDoubleValue, ctx); + EXPECT_EQ(details2.GetFlagKey(), flag_key); + EXPECT_DOUBLE_EQ(details2.GetValue(), kDefaultDoubleValue); + + auto details3 = + client.GetDoubleDetails(flag_key, kDefaultDoubleValue, options); + EXPECT_EQ(details3.GetFlagKey(), flag_key); + EXPECT_DOUBLE_EQ(details3.GetValue(), kDefaultDoubleValue); + + auto details4 = + client.GetDoubleDetails(flag_key, kDefaultDoubleValue, ctx, options); + EXPECT_EQ(details4.GetFlagKey(), flag_key); + EXPECT_DOUBLE_EQ(details4.GetValue(), kDefaultDoubleValue); +} + +// Test GetObjectDetails returns proper details with NoopProvider. +TEST_F(ClientAPITest, GetObjectDetailsReturnsDetailsWithNoopProvider) { + ClientAPI client(repo_, "test-domain"); + std::string flag_key = "my-object-flag"; + EvaluationContext ctx = EvaluationContext::Builder().build(); + EvaluationOptions options; + + auto details1 = + client.GetObjectDetails(flag_key, Value(kDefaultObjectIntValue)); + EXPECT_EQ(details1.GetFlagKey(), flag_key); + EXPECT_EQ(details1.GetValue(), Value(kDefaultObjectIntValue)); + EXPECT_EQ(details1.GetReason(), Reason::kDefault); + EXPECT_EQ(details1.GetErrorCode(), std::nullopt); + + auto details2 = + client.GetObjectDetails(flag_key, Value(kDefaultObjectIntValue), ctx); + EXPECT_EQ(details2.GetFlagKey(), flag_key); + EXPECT_EQ(details2.GetValue(), Value(kDefaultObjectIntValue)); + + auto details3 = + client.GetObjectDetails(flag_key, Value(kDefaultObjectIntValue), options); + EXPECT_EQ(details3.GetFlagKey(), flag_key); + EXPECT_EQ(details3.GetValue(), Value(kDefaultObjectIntValue)); + + auto details4 = client.GetObjectDetails( + flag_key, Value(kDefaultObjectIntValue), ctx, options); + EXPECT_EQ(details4.GetFlagKey(), flag_key); + EXPECT_EQ(details4.GetValue(), Value(kDefaultObjectIntValue)); +} + // Test context merging logic indirectly. TEST_F(ClientAPITest, ContextMergingPrecedence) { GlobalContextManager::GetInstance().SetGlobalEvaluationContext( @@ -267,6 +492,49 @@ TEST_F(ClientAPITest, EvaluateFlagHandlesProviderErrorStatus) { EXPECT_TRUE(client.GetBooleanValue("flag", true)); } +TEST_F(ClientAPITest, GetDetailsPropagatesProviderVariantAndMetadata) { + auto mock_provider = std::make_shared>(); + FlagMetadata metadata; + metadata.data["meta_key"] = std::string("meta_val"); + + EXPECT_CALL(*mock_provider, GetBooleanEvaluation("flag", false, _)) + .WillOnce(Return(std::make_unique( + true, Reason::kTargetingMatch, "variant_a", metadata))); + + repo_.SetProvider("test-domain", mock_provider, + EvaluationContext::Builder().build(), true); + ClientAPI client(repo_, "test-domain"); + + auto details = client.GetBooleanDetails("flag", false); + EXPECT_EQ(details.GetFlagKey(), "flag"); + EXPECT_TRUE(details.GetValue()); + EXPECT_EQ(details.GetReason(), Reason::kTargetingMatch); + EXPECT_EQ(details.GetVariant(), "variant_a"); + ASSERT_EQ(details.GetFlagMetadata().data.count("meta_key"), 1); + EXPECT_EQ( + std::get(details.GetFlagMetadata().data.at("meta_key")), + "meta_val"); + EXPECT_EQ(details.GetErrorCode(), std::nullopt); +} + +TEST_F(ClientAPITest, GetDetailsHandlesProviderErrors) { + auto mock_provider = std::make_shared>(); + + EXPECT_CALL(*mock_provider, GetBooleanEvaluation("flag", false, _)) + .WillOnce(Return(absl::InternalError("Provider failed"))); + + repo_.SetProvider("test-domain", mock_provider, + EvaluationContext::Builder().build(), true); + ClientAPI client(repo_, "test-domain"); + + auto details = client.GetBooleanDetails("flag", false); + EXPECT_EQ(details.GetFlagKey(), "flag"); + EXPECT_FALSE(details.GetValue()); + EXPECT_EQ(details.GetReason(), Reason::kError); + EXPECT_EQ(details.GetErrorCode(), ErrorCode::kGeneral); + EXPECT_EQ(details.GetErrorMessage(), "Provider failed"); +} + TEST_F(ClientAPITest, EvaluateFlagHandlesProviderNullResolutionDetails) { auto mock_provider = std::make_shared>(); @@ -440,3 +708,68 @@ TEST_F(ClientAPITest, ParallelProviderSwapRaceCondition) { evaluation_thread.join(); proceed_init->set_value(); } + +namespace { +class DummyHook1 : public openfeature::BoolHook {}; +class DummyHook2 : public openfeature::StringHook {}; +} // namespace + +// Test that client is initialized with empty hooks by default. +TEST_F(ClientAPITest, InitialStateHasEmptyHooks) { + ClientAPI client(repo_, "test-domain"); + EXPECT_TRUE(client.GetHooks().empty()); +} + +// Test adding a single hook via AddHook. +TEST_F(ClientAPITest, AddHookAppendsSingleHook) { + ClientAPI client(repo_, "test-domain"); + std::shared_ptr hook1 = + std::make_shared(); + client.AddHook(hook1); + + auto hooks = client.GetHooks(); + ASSERT_EQ(hooks.size(), 1); + EXPECT_EQ(hooks[0], hook1); +} + +// Test adding multiple hooks via AddHooks and preserving registration order. +TEST_F(ClientAPITest, AddHooksAppendsMultipleHooksAndPreservesOrder) { + ClientAPI client(repo_, "test-domain"); + std::shared_ptr hook1 = + std::make_shared(); + std::shared_ptr hook2 = + std::make_shared(); + + client.AddHooks({hook1, hook2}); + + auto hooks = client.GetHooks(); + ASSERT_EQ(hooks.size(), 2); + EXPECT_EQ(hooks[0], hook1); + EXPECT_EQ(hooks[1], hook2); + + // Adding another hook appends without clearing existing ones + std::shared_ptr hook3 = + std::make_shared(); + client.AddHook(hook3); + + hooks = client.GetHooks(); + ASSERT_EQ(hooks.size(), 3); + EXPECT_EQ(hooks[0], hook1); + EXPECT_EQ(hooks[1], hook2); + EXPECT_EQ(hooks[2], hook3); +} + +// Test that AddHook and AddHooks filter out nullptr entries. +TEST_F(ClientAPITest, AddHookAndAddHooksFiltersNullptrs) { + ClientAPI client(repo_, "test-domain"); + client.AddHook(nullptr); + EXPECT_TRUE(client.GetHooks().empty()); + + std::shared_ptr valid_hook = + std::make_shared(); + client.AddHooks({nullptr, valid_hook, nullptr}); + + auto hooks = client.GetHooks(); + ASSERT_EQ(hooks.size(), 1); + EXPECT_EQ(hooks[0], valid_hook); +}