diff --git a/openfeature/BUILD b/openfeature/BUILD index 1578b39..162621d 100644 --- a/openfeature/BUILD +++ b/openfeature/BUILD @@ -49,6 +49,7 @@ cc_library( deps = [ ":evaluation_context", ":features", + ":general_hook", ":metadata", ":provider_status", ], @@ -64,6 +65,7 @@ cc_library( ":evaluation_context", ":features", ":flag_metadata", + ":general_hook", ":global_context_manager", ":metadata", ":provider", 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..73d6260 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; } @@ -151,9 +151,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..8ea62da 100644 --- a/openfeature/client_api.h +++ b/openfeature/client_api.h @@ -6,10 +6,12 @@ #include #include #include +#include #include "openfeature/client.h" #include "openfeature/evaluation_context.h" #include "openfeature/features.h" +#include "openfeature/general_hook.h" #include "openfeature/global_context_manager.h" #include "openfeature/metadata.h" #include "openfeature/provider.h" @@ -68,7 +70,15 @@ class ClientAPI : public Client { Value GetObjectValue(std::string_view flag_key, Value default_value, const EvaluationContext& ctx) override; - // TODO: Add methods to get and set Hooks. + // 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; + + // Retrieves all configured client-level hooks. + std::vector> GetHooks() const override; + // TODO: Add methods for detailed flag evaluation. // TODO: Overload method "GetBooleanValue" to accept "Evaluation Options". @@ -106,6 +116,8 @@ 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 #include #include +#pragma endregion #include "openfeature/client.h" #include "openfeature/evaluation_context.h" diff --git a/test/BUILD b/test/BUILD index 799e0bb..90d2282 100644 --- a/test/BUILD +++ b/test/BUILD @@ -20,6 +20,7 @@ cc_test( deps = [ ":mock_feature_provider", "//openfeature:client_api", + "//openfeature:hook", "@googletest//:gtest_main", ], ) diff --git a/test/client_api_test.cpp b/test/client_api_test.cpp index 5008f36..d3eb534 100644 --- a/test/client_api_test.cpp +++ b/test/client_api_test.cpp @@ -14,6 +14,7 @@ #include "mocks/mock_feature_provider.h" #include "openfeature/evaluation_context.h" #include "openfeature/global_context_manager.h" +#include "openfeature/hook.h" #include "openfeature/provider_status.h" using ::openfeature::BoolResolutionDetails; @@ -440,3 +441,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); +}