diff --git a/openfeature/BUILD b/openfeature/BUILD index 7283fc5..1578b39 100644 --- a/openfeature/BUILD +++ b/openfeature/BUILD @@ -217,6 +217,7 @@ cc_library( hdrs = ["openfeature_api.h"], include_prefix = "openfeature", deps = [ + ":general_hook", ":client", ":client_api", ":evaluation_context", @@ -249,6 +250,7 @@ cc_library( hdrs = ["openfeature.h"], include_prefix = "openfeature", deps = [ + ":general_hook", ":client", ":evaluation_context", ":metadata", diff --git a/openfeature/flag_evaluation_details.h b/openfeature/flag_evaluation_details.h index c9ef595..f4aeef1 100644 --- a/openfeature/flag_evaluation_details.h +++ b/openfeature/flag_evaluation_details.h @@ -55,4 +55,4 @@ using ObjectFlagEvaluationDetails = FlagEvaluationDetails; } // namespace openfeature -#endif // CPP_SDK_INCLUDE_OPENFEATURE_FLAG_EVALUATION_DETAILS_H_ +#endif // CPP_SDK_INCLUDE_OPENFEATURE_FLAG_EVALUATION_DETAILS_H_ \ No newline at end of file diff --git a/openfeature/hook.h b/openfeature/hook.h index ba28b69..993a8ea 100644 --- a/openfeature/hook.h +++ b/openfeature/hook.h @@ -98,4 +98,4 @@ using DoubleHook = Hook; using ObjectHook = Hook; } // namespace openfeature -#endif // CPP_SDK_INCLUDE_OPENFEATURE_HOOK_H_ +#endif // CPP_SDK_INCLUDE_OPENFEATURE_HOOK_H_ \ No newline at end of file diff --git a/openfeature/openfeature.h b/openfeature/openfeature.h index 3f959d7..1335f4f 100644 --- a/openfeature/openfeature.h +++ b/openfeature/openfeature.h @@ -5,9 +5,11 @@ #include #include #include +#include #include "openfeature/client.h" #include "openfeature/evaluation_context.h" +#include "openfeature/general_hook.h" #include "openfeature/metadata.h" #include "openfeature/provider.h" @@ -35,8 +37,9 @@ class OpenFeature { // If the domain is empty then GetProvider returns the default provider // otherwise it returns the provider for the domain. If this domain has no // provider bound, it returns the default provider. + virtual std::shared_ptr GetProvider() const = 0; virtual std::shared_ptr GetProvider( - std::string_view domain = "") const = 0; + std::string_view domain) const = 0; virtual std::shared_ptr GetClient() = 0; @@ -48,18 +51,27 @@ class OpenFeature { // Gets the global evaluation context. virtual EvaluationContext GetEvaluationContext() const = 0; - // Gets the metadata for a provider bound to a specific domain. - virtual Metadata GetProviderMetadata(std::string_view domain = "") const = 0; + // Gets the metadata for the default provider or a provider bound to a + // specific domain. + virtual Metadata GetProviderMetadata() const = 0; + virtual Metadata GetProviderMetadata(std::string_view domain) const = 0; // Fetches the status of a provider for a domain. If the domain is not set or // not found, it returns the default provider status. - virtual ProviderStatus GetProviderStatus( - std::string_view domain = "") const = 0; + virtual ProviderStatus GetProviderStatus() const = 0; + virtual ProviderStatus GetProviderStatus(std::string_view domain) const = 0; + + // Adds one or more global hooks. Previously added hooks are not removed. + virtual void AddHooks(std::vector> hooks) = 0; + + // Adds a single hook to the global hook repository. + virtual void AddHook(std::shared_ptr hook) = 0; + + // Retrieves all configured global hooks. + virtual std::vector> GetHooks() const = 0; // Shuts down all providers and resets the API to its initial state. virtual void Shutdown() = 0; - - // TODO: Add methods to add and get Hooks. }; } // namespace openfeature diff --git a/openfeature/openfeature_api.cpp b/openfeature/openfeature_api.cpp index e839764..e407161 100644 --- a/openfeature/openfeature_api.cpp +++ b/openfeature/openfeature_api.cpp @@ -5,11 +5,7 @@ namespace openfeature { -OpenFeatureAPI::OpenFeatureAPI() { - // provider_repository_ is automatically constructed. - // It guarantees a NoopProvider is set by default. - // TODO: init hooks & events. -} +OpenFeatureAPI::OpenFeatureAPI() = default; OpenFeatureAPI& OpenFeatureAPI::GetInstance() { static OpenFeatureAPI instance; @@ -43,6 +39,10 @@ void OpenFeatureAPI::SetProviderAndWait( GlobalContextManager::GetInstance().GetGlobalEvaluationContext(), true); } +std::shared_ptr OpenFeatureAPI::GetProvider() const { + return GetProvider(""); +} + std::shared_ptr OpenFeatureAPI::GetProvider( std::string_view domain) const { return provider_repository_.GetProvider(domain); @@ -63,13 +63,21 @@ EvaluationContext OpenFeatureAPI::GetEvaluationContext() const { return GlobalContextManager::GetInstance().GetGlobalEvaluationContext(); } +Metadata OpenFeatureAPI::GetProviderMetadata() const { + return GetProviderMetadata(""); +} + Metadata OpenFeatureAPI::GetProviderMetadata(std::string_view domain) const { std::shared_ptr provider = provider_repository_.GetProvider(domain); if (provider) { return provider->GetMetadata(); } - return Metadata(); // Return empty metadata if provider not found + return {}; // Return empty metadata if provider not found +} + +ProviderStatus OpenFeatureAPI::GetProviderStatus() const { + return GetProviderStatus(""); } ProviderStatus OpenFeatureAPI::GetProviderStatus( @@ -77,6 +85,31 @@ ProviderStatus OpenFeatureAPI::GetProviderStatus( return provider_repository_.GetProviderStatus(domain); } -void OpenFeatureAPI::Shutdown() { provider_repository_.Shutdown(); } +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)); + } + } +} + +void OpenFeatureAPI::AddHook(std::shared_ptr hook) { + if (hook == nullptr) return; + std::unique_lock lock(hooks_mutex_); + hooks_.push_back(std::move(hook)); +} + +std::vector> OpenFeatureAPI::GetHooks() const { + std::shared_lock lock(hooks_mutex_); + return hooks_; +} + +void OpenFeatureAPI::Shutdown() { + provider_repository_.Shutdown(); + std::unique_lock lock(hooks_mutex_); + hooks_.clear(); +} } // namespace openfeature \ No newline at end of file diff --git a/openfeature/openfeature_api.h b/openfeature/openfeature_api.h index 840f31c..a9fb4b9 100644 --- a/openfeature/openfeature_api.h +++ b/openfeature/openfeature_api.h @@ -7,6 +7,7 @@ #include "openfeature/client.h" #include "openfeature/evaluation_context.h" +#include "openfeature/general_hook.h" #include "openfeature/global_context_manager.h" #include "openfeature/metadata.h" #include "openfeature/openfeature.h" @@ -19,7 +20,7 @@ namespace openfeature { // library. class OpenFeatureAPI : public OpenFeature { public: - ~OpenFeatureAPI() = default; + ~OpenFeatureAPI() override = default; // Get the singleton instance of the OpenFeatureAPI. static OpenFeatureAPI& GetInstance(); @@ -43,8 +44,9 @@ class OpenFeatureAPI : public OpenFeature { // If the domain is empty then GetProvider returns the default provider // otherwise it returns the provider for the domain. If this domain has no // provider bound, it returns the default provider. + std::shared_ptr GetProvider() const override; std::shared_ptr GetProvider( - std::string_view domain = "") const override; + std::string_view domain) const override; // Gets a client for the default domain. std::shared_ptr GetClient() override; @@ -60,21 +62,30 @@ class OpenFeatureAPI : public OpenFeature { // Get metadata about the default provider if domain is empty // or about a named provider if domain is provided. - Metadata GetProviderMetadata(std::string_view domain = "") const override; + Metadata GetProviderMetadata() const override; + Metadata GetProviderMetadata(std::string_view domain) const override; // Fetches the status of a provider for a domain. If the domain is not set or // not found, it returns the default provider status. - ProviderStatus GetProviderStatus(std::string_view domain = "") const override; + ProviderStatus GetProviderStatus() const override; + ProviderStatus GetProviderStatus(std::string_view domain) const override; + + // Adds one or more global hooks. Previously added hooks are not removed. + void AddHooks(std::vector> hooks) override; + + // Adds a single hook to the global hook repository. + void AddHook(std::shared_ptr hook) override; + + // Retrieves all configured global hooks. + std::vector> GetHooks() const override; // Shuts down all providers and resets the API to its initial state. void Shutdown() override; - // TODO: Add methods to add and get Hooks. - // TODO: Add overload function for "GetClient()" to accept "Evaluation - // Options" - private: ProviderRepository provider_repository_; + mutable std::shared_mutex hooks_mutex_; + std::vector> hooks_; OpenFeatureAPI(); }; diff --git a/test/BUILD b/test/BUILD index fcbeca4..799e0bb 100644 --- a/test/BUILD +++ b/test/BUILD @@ -85,6 +85,8 @@ cc_test( srcs = ["openfeature_api_test.cpp"], deps = [ ":mock_feature_provider", + "//openfeature:general_hook", + "//openfeature:hook", "//openfeature:openfeature_api", "@googletest//:gtest_main", ], diff --git a/test/hook_test.cpp b/test/hook_test.cpp index ed9796d..418b7a5 100644 --- a/test/hook_test.cpp +++ b/test/hook_test.cpp @@ -327,4 +327,4 @@ TEST_F(HookTest, TypedHookIgnoresMismatchedFlagTypes) { EXPECT_FALSE(bool_hook.after_called); } -} // namespace openfeature \ No newline at end of file +} // namespace openfeature diff --git a/test/openfeature_api_test.cpp b/test/openfeature_api_test.cpp index ec6ea6c..a8f1e97 100644 --- a/test/openfeature_api_test.cpp +++ b/test/openfeature_api_test.cpp @@ -7,6 +7,7 @@ #include #include "mocks/mock_feature_provider.h" +#include "openfeature/hook.h" #include "openfeature/noop_provider.h" using ::testing::_; @@ -171,7 +172,6 @@ TEST_F(OpenFeatureAPITest, SetNamedProviderAsyncDoesNotBlock) { init_can_complete_future.wait(); init_has_finished.set_value(); return absl::OkStatus(); - ; }); EXPECT_CALL(*mock_provider, Shutdown()).WillOnce(Return(absl::OkStatus())); api_.SetProvider(domain, mock_provider); @@ -201,7 +201,137 @@ TEST_F(OpenFeatureAPITest, GetNamedClient) { EXPECT_EQ(named_client->GetMetadata().name, "some-domain"); } -// TODO: Add tests for "GetEvaluationContext" and "SetEvaluationContext" once. -// EvaluationContext logic is implemented. +// Test that default global evaluation context is empty. +TEST_F(OpenFeatureAPITest, DefaultEvaluationContextIsEmpty) { + EvaluationContext ctx = api_.GetEvaluationContext(); + EXPECT_FALSE(ctx.GetTargetingKey().has_value()); + EXPECT_TRUE(ctx.GetAttributes().empty()); +} + +// Test setting and retrieving the global evaluation context. +TEST_F(OpenFeatureAPITest, SetAndGetGlobalEvaluationContext) { + EvaluationContext new_ctx = + EvaluationContext::Builder() + .WithTargetingKey("global-user-123") + .WithAttribute("environment", std::string("production")) + .WithAttribute("app_version", std::string("2.1.0")) + .build(); + + api_.SetEvaluationContext(new_ctx); + + EvaluationContext retrieved_ctx = api_.GetEvaluationContext(); + ASSERT_TRUE(retrieved_ctx.GetTargetingKey().has_value()); + EXPECT_EQ(retrieved_ctx.GetTargetingKey().value(), "global-user-123"); + + const std::any* env_val = retrieved_ctx.GetValue("environment"); + ASSERT_NE(env_val, nullptr); + EXPECT_EQ(std::any_cast(*env_val), "production"); + + const std::any* version_val = retrieved_ctx.GetValue("app_version"); + ASSERT_NE(version_val, nullptr); + EXPECT_EQ(std::any_cast(*version_val), "2.1.0"); +} + +// Test overwriting an existing global evaluation context. +TEST_F(OpenFeatureAPITest, OverwriteGlobalEvaluationContext) { + EvaluationContext first_ctx = + EvaluationContext::Builder().WithTargetingKey("user-initial").build(); + api_.SetEvaluationContext(first_ctx); + EXPECT_EQ(api_.GetEvaluationContext().GetTargetingKey().value(), + "user-initial"); + + EvaluationContext updated_ctx = + EvaluationContext::Builder().WithTargetingKey("user-updated").build(); + api_.SetEvaluationContext(updated_ctx); + EXPECT_EQ(api_.GetEvaluationContext().GetTargetingKey().value(), + "user-updated"); +} + +class DummyHook1 : public BoolHook {}; +class DummyHook2 : public StringHook {}; + +// Test that GetHooks returns an empty vector initially. +TEST_F(OpenFeatureAPITest, InitialStateHasEmptyHooks) { + EXPECT_TRUE(api_.GetHooks().empty()); +} + +// Test adding a single hook via AddHook. +TEST_F(OpenFeatureAPITest, AddHookAppendsSingleHook) { + std::shared_ptr hook1 = std::make_shared(); + api_.AddHook(hook1); + + auto hooks = api_.GetHooks(); + ASSERT_EQ(hooks.size(), 1); + EXPECT_EQ(hooks[0], hook1); +} + +// Test adding multiple hooks via AddHooks and preserving registration order. +TEST_F(OpenFeatureAPITest, AddHooksAppendsMultipleHooksAndPreservesOrder) { + std::shared_ptr hook1 = std::make_shared(); + std::shared_ptr hook2 = std::make_shared(); + + api_.AddHooks({hook1, hook2}); + + auto hooks = api_.GetHooks(); + ASSERT_EQ(hooks.size(), 2); + EXPECT_EQ(hooks[0], hook1); + EXPECT_EQ(hooks[1], hook2); + + // Adding another hook appends without clearing existing ones (Req 1.1.4) + std::shared_ptr hook3 = std::make_shared(); + api_.AddHook(hook3); + + hooks = api_.GetHooks(); + ASSERT_EQ(hooks.size(), 3); + EXPECT_EQ(hooks[0], hook1); + EXPECT_EQ(hooks[1], hook2); + EXPECT_EQ(hooks[2], hook3); +} +// Test fetching status for default and named providers. +TEST_F(OpenFeatureAPITest, GetProviderStatusDefaultAndNamed) { + // Default provider (NoopProvider) is READY upon initialization + EXPECT_EQ(api_.GetProviderStatus(), ProviderStatus::kReady); + + std::shared_ptr mock_provider = + std::make_shared(); + std::string domain = "status-domain"; + + EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); + + // Prior to registration, provider status for unknown domain returns default + // status (kReady) + EXPECT_EQ(api_.GetProviderStatus(domain), ProviderStatus::kReady); + + api_.SetProviderAndWait(domain, mock_provider); + + EXPECT_EQ(api_.GetProviderStatus(domain), ProviderStatus::kReady); +} + +// Test that AddHook and AddHooks filter out nullptr entries. +TEST_F(OpenFeatureAPITest, AddHookAndAddHooksFiltersNullptrs) { + api_.AddHook(nullptr); + EXPECT_TRUE(api_.GetHooks().empty()); + + std::shared_ptr valid_hook = std::make_shared(); + api_.AddHooks({nullptr, valid_hook, nullptr}); + + auto hooks = api_.GetHooks(); + ASSERT_EQ(hooks.size(), 1); + EXPECT_EQ(hooks[0], valid_hook); +} + +// Test that Shutdown clears all registered global hooks (Req 1.6.2). +TEST_F(OpenFeatureAPITest, ShutdownClearsAllGlobalHooks) { + std::shared_ptr hook1 = std::make_shared(); + std::shared_ptr hook2 = std::make_shared(); + api_.AddHooks({hook1, hook2}); + + ASSERT_EQ(api_.GetHooks().size(), 2); + + api_.Shutdown(); + + EXPECT_TRUE(api_.GetHooks().empty()) + << "Shutdown must clear all registered global hooks."; +} } // namespace openfeature