diff --git a/openfeature/BUILD b/openfeature/BUILD index 2c82f6e..ce85e8c 100644 --- a/openfeature/BUILD +++ b/openfeature/BUILD @@ -93,6 +93,23 @@ cc_library( include_prefix = "openfeature", ) +cc_library( + name = "openfeature_api", + srcs = ["openfeature_api.cpp"], + hdrs = ["openfeature_api.h"], + include_prefix = "openfeature", + deps = [ + ":client", + ":client_api", + ":evaluation_context", + ":global_context_manager", + ":metadata", + ":openfeature", + ":provider", + ":provider_repository", + ], +) + cc_library( name = "noop_provider", srcs = ["noop_provider.cpp"], diff --git a/openfeature/openfeature.h b/openfeature/openfeature.h index ac3b24b..3f959d7 100644 --- a/openfeature/openfeature.h +++ b/openfeature/openfeature.h @@ -28,25 +28,15 @@ class OpenFeature { virtual void SetProviderAndWait( std::shared_ptr provider) = 0; - // Sets the default provider and blocks until it initializes or a timeout - // occurs. - virtual void SetProviderAndWait(std::shared_ptr provider, - std::chrono::milliseconds timeout) = 0; - // Sets a named provider and blocks until it successfully initializes. virtual void SetProviderAndWait( std::string_view domain, std::shared_ptr provider) = 0; - // Sets a named provider and blocks until it initializes or a timeout occurs. - virtual void SetProviderAndWait(std::string_view domain, - std::shared_ptr provider, - std::chrono::milliseconds timeout) = 0; - // 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( - std::string_view domain = "") = 0; + std::string_view domain = "") const = 0; virtual std::shared_ptr GetClient() = 0; @@ -55,17 +45,21 @@ class OpenFeature { // Sets the global evaluation context. virtual void SetEvaluationContext(const EvaluationContext& ctx) = 0; - // Gets the global evaluation context - virtual EvaluationContext GetEvaluationContext( - std::shared_mutex& mutex, const EvaluationContext& ctx_src) = 0; + // 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 = "") = 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; // Shuts down all providers and resets the API to its initial state. virtual void Shutdown() = 0; - // TODO: Add methods to add and get Hooks + // TODO: Add methods to add and get Hooks. }; } // namespace openfeature diff --git a/openfeature/openfeature_api.cpp b/openfeature/openfeature_api.cpp new file mode 100644 index 0000000..e839764 --- /dev/null +++ b/openfeature/openfeature_api.cpp @@ -0,0 +1,82 @@ +#include "openfeature/openfeature_api.h" + +#include "openfeature/client_api.h" +#include "openfeature/global_context_manager.h" + +namespace openfeature { + +OpenFeatureAPI::OpenFeatureAPI() { + // provider_repository_ is automatically constructed. + // It guarantees a NoopProvider is set by default. + // TODO: init hooks & events. +} + +OpenFeatureAPI& OpenFeatureAPI::GetInstance() { + static OpenFeatureAPI instance; + return instance; +} + +void OpenFeatureAPI::SetProvider(std::shared_ptr provider) { + provider_repository_.SetProvider( + provider, + GlobalContextManager::GetInstance().GetGlobalEvaluationContext(), false); +} + +void OpenFeatureAPI::SetProvider(std::string_view domain, + std::shared_ptr provider) { + provider_repository_.SetProvider( + domain, provider, + GlobalContextManager::GetInstance().GetGlobalEvaluationContext(), false); +} + +void OpenFeatureAPI::SetProviderAndWait( + std::shared_ptr provider) { + provider_repository_.SetProvider( + provider, + GlobalContextManager::GetInstance().GetGlobalEvaluationContext(), true); +} + +void OpenFeatureAPI::SetProviderAndWait( + std::string_view domain, std::shared_ptr provider) { + provider_repository_.SetProvider( + domain, provider, + GlobalContextManager::GetInstance().GetGlobalEvaluationContext(), true); +} + +std::shared_ptr OpenFeatureAPI::GetProvider( + std::string_view domain) const { + return provider_repository_.GetProvider(domain); +} + +std::shared_ptr OpenFeatureAPI::GetClient() { return GetClient(""); } + +std::shared_ptr OpenFeatureAPI::GetClient(std::string_view domain) { + auto client = std::make_shared(provider_repository_, domain); + return client; +} + +void OpenFeatureAPI::SetEvaluationContext(const EvaluationContext& ctx) { + GlobalContextManager::GetInstance().SetGlobalEvaluationContext(ctx); +} + +EvaluationContext OpenFeatureAPI::GetEvaluationContext() const { + return GlobalContextManager::GetInstance().GetGlobalEvaluationContext(); +} + +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 +} + +ProviderStatus OpenFeatureAPI::GetProviderStatus( + std::string_view domain) const { + return provider_repository_.GetProviderStatus(domain); +} + +void OpenFeatureAPI::Shutdown() { provider_repository_.Shutdown(); } + +} // namespace openfeature \ No newline at end of file diff --git a/openfeature/openfeature_api.h b/openfeature/openfeature_api.h new file mode 100644 index 0000000..840f31c --- /dev/null +++ b/openfeature/openfeature_api.h @@ -0,0 +1,84 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_OPENFEATURE_API_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_OPENFEATURE_API_H_ + +#include +#include +#include + +#include "openfeature/client.h" +#include "openfeature/evaluation_context.h" +#include "openfeature/global_context_manager.h" +#include "openfeature/metadata.h" +#include "openfeature/openfeature.h" +#include "openfeature/provider.h" +#include "openfeature/provider_repository.h" + +namespace openfeature { + +// A global singleton which holds base configuration for the OpenFeature +// library. +class OpenFeatureAPI : public OpenFeature { + public: + ~OpenFeatureAPI() = default; + + // Get the singleton instance of the OpenFeatureAPI. + static OpenFeatureAPI& GetInstance(); + + OpenFeatureAPI(const OpenFeatureAPI&) = delete; + OpenFeatureAPI& operator=(const OpenFeatureAPI&) = delete; + + // Set the default provider. + void SetProvider(std::shared_ptr provider) override; + + // Set a provider for a specific domain. + void SetProvider(std::string_view domain, + std::shared_ptr provider) override; + + // Set the default provider and blocks until it successfully initializes. + void SetProviderAndWait(std::shared_ptr provider) override; + + // Set a named provider and blocks until it successfully initializes. + void SetProviderAndWait(std::string_view domain, + std::shared_ptr provider) override; + // 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( + std::string_view domain = "") const override; + + // Gets a client for the default domain. + std::shared_ptr GetClient() override; + + // Gets a client for a named domain. + std::shared_ptr GetClient(std::string_view domain) override; + + // Sets the global evaluation context. + void SetEvaluationContext(const EvaluationContext& ctx) override; + + // Gets the global evaluation context. + EvaluationContext GetEvaluationContext() const override; + + // 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; + + // 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; + + // 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_; + + OpenFeatureAPI(); +}; + +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_OPENFEATURE_API_H_ diff --git a/openfeature/provider_repository.cpp b/openfeature/provider_repository.cpp index 9ae404b..e15bb88 100644 --- a/openfeature/provider_repository.cpp +++ b/openfeature/provider_repository.cpp @@ -115,6 +115,16 @@ void ProviderRepository::Shutdown() { } } provider_manager_.clear(); + + // Re-initialize to the default state after shutting down + std::shared_ptr noop_provider = + std::make_shared(); + absl::StatusOr> status_manager = + FeatureProviderStatusManager::Create(noop_provider); + if (status_manager.ok()) { + default_manager_ = std::move(status_manager.value()); + default_manager_->SetStatus(ProviderStatus::kReady); + } } void ProviderRepository::PrepareAndInitializeProvider( diff --git a/test/BUILD b/test/BUILD index 620c958..dea3b24 100644 --- a/test/BUILD +++ b/test/BUILD @@ -59,6 +59,16 @@ cc_test( ], ) +cc_test( + name = "openfeature_api_test", + srcs = ["openfeature_api_test.cpp"], + deps = [ + ":mock_feature_provider", + "//openfeature:openfeature_api", + "@googletest//:gtest_main", + ], +) + cc_test( name = "global_context_manager_test", srcs = ["global_context_manager_test.cpp"], diff --git a/test/global_context_manager_test.cpp b/test/global_context_manager_test.cpp index 9986f4e..751fb36 100644 --- a/test/global_context_manager_test.cpp +++ b/test/global_context_manager_test.cpp @@ -82,8 +82,3 @@ TEST_F(GlobalContextManagerTest, ThreadSafetyStressTest) { t.join(); } } - -int main(int argc, char** argv) { - testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} \ No newline at end of file diff --git a/test/openfeature_api_test.cpp b/test/openfeature_api_test.cpp new file mode 100644 index 0000000..f2b9e5a --- /dev/null +++ b/test/openfeature_api_test.cpp @@ -0,0 +1,196 @@ +#include "openfeature/openfeature_api.h" + +#include +#include + +#include +#include + +#include "mocks/mock_feature_provider.h" +#include "openfeature/noop_provider.h" + +using namespace openfeature; +using ::testing::_; +using ::testing::Return; + +class OpenFeatureAPITest : public ::testing::Test { + protected: + // To ensure test isolation for the singleton, we shut it down before and + // after each test, to reset it to its default state. + void SetUp() override {} + void TearDown() override { + api.Shutdown(); + api.SetEvaluationContext(EvaluationContext{}); + } + + OpenFeatureAPI& api = OpenFeatureAPI::GetInstance(); +}; + +// Test that GetInstance always returns the same singleton instance. +TEST_F(OpenFeatureAPITest, GetInstanceReturnsSameInstance) { + OpenFeatureAPI& instance1 = OpenFeatureAPI::GetInstance(); + OpenFeatureAPI& instance2 = OpenFeatureAPI::GetInstance(); + ASSERT_EQ(&instance1, &instance2); +} + +// Test that the API is initialized with a NoopProvider by default. +TEST_F(OpenFeatureAPITest, InitialStateHasNoopProvider) { + std::shared_ptr provider = api.GetProvider(); + ASSERT_NE(provider, nullptr); + EXPECT_NE(dynamic_cast(provider.get()), nullptr); +} + +// Test setting the default provider and waiting for its initialization. +TEST_F(OpenFeatureAPITest, SetAndGetDefaultProviderAndWait) { + std::shared_ptr mock_provider = + std::make_shared(); + EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); + + api.SetProviderAndWait(mock_provider); + + EXPECT_EQ(api.GetProvider(), mock_provider); +} + +// Test setting a named provider and waiting for its initialization. +TEST_F(OpenFeatureAPITest, SetAndGetNamedProviderAndWait) { + std::shared_ptr mock_provider = + std::make_shared(); + std::string domain = "test-domain"; + EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); + + api.SetProviderAndWait(domain, mock_provider); + + EXPECT_EQ(api.GetProvider(domain), mock_provider); + EXPECT_NE(dynamic_cast(api.GetProvider().get()), nullptr); +} + +// Test that getting a provider for a non-existent domain falls back to the +// default. +TEST_F(OpenFeatureAPITest, GetProviderFallsBackToDefault) { + std::shared_ptr default_provider = api.GetProvider(); + std::shared_ptr unknown_domain_provider = + api.GetProvider("unknown-domain"); + EXPECT_EQ(default_provider, unknown_domain_provider); +} + +// Test getting metadata from the default provider. +TEST_F(OpenFeatureAPITest, GetProviderMetadataForDefault) { + Metadata metadata = api.GetProviderMetadata(); + EXPECT_EQ(metadata.name, "Noop Provider"); +} + +// Test getting metadata from a named provider. +TEST_F(OpenFeatureAPITest, GetProviderMetadataForNamed) { + std::shared_ptr mock_provider = + std::make_shared(); + std::string domain = "metadata-domain"; + Metadata expected_metadata; + expected_metadata.name = "Mock Provider"; + + EXPECT_CALL(*mock_provider, GetMetadata()) + .WillOnce(Return(expected_metadata)); + EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); + + api.SetProviderAndWait(domain, mock_provider); + Metadata actual_metadata = api.GetProviderMetadata(domain); + + EXPECT_EQ(actual_metadata.name, expected_metadata.name); +} + +// Test that the Shutdown method calls Shutdown on all registered providers. +TEST_F(OpenFeatureAPITest, ShutdownCallsProviderShutdown) { + std::shared_ptr mock_default_provider = + std::make_shared(); + std::shared_ptr mock_named_provider = + std::make_shared(); + std::string domain = "shutdown-domain"; + + EXPECT_CALL(*mock_default_provider, Init(_)) + .WillOnce(Return(absl::OkStatus())); + EXPECT_CALL(*mock_named_provider, Init(_)).WillOnce(Return(absl::OkStatus())); + + api.SetProviderAndWait(mock_default_provider); + api.SetProviderAndWait(domain, mock_named_provider); + + EXPECT_CALL(*mock_default_provider, Shutdown()) + .WillOnce(Return(absl::OkStatus())); + EXPECT_CALL(*mock_named_provider, Shutdown()) + .WillOnce(Return(absl::OkStatus())); + + api.Shutdown(); + + testing::Mock::VerifyAndClearExpectations(mock_default_provider.get()); + testing::Mock::VerifyAndClearExpectations(mock_named_provider.get()); +} + +// Test the asynchronous SetProvider to ensure it doesn't block. +TEST_F(OpenFeatureAPITest, SetProviderAsyncDoesNotBlock) { + std::shared_ptr mock_provider = + std::make_shared(); + std::promise init_can_start; + std::future init_started_future = init_can_start.get_future(); + std::promise init_can_complete; + std::future init_can_complete_future = init_can_complete.get_future(); + + EXPECT_CALL(*mock_provider, Init(_)).WillOnce([&](const auto&) { + init_can_start.set_value(); + init_can_complete_future.wait(); + return absl::OkStatus(); + }); + EXPECT_CALL(*mock_provider, Shutdown()).WillOnce(Return(absl::OkStatus())); + api.SetProvider(mock_provider); + + // Confirm the background task has started. + auto status = init_started_future.wait_for(std::chrono::seconds(1)); + ASSERT_EQ(status, std::future_status::ready) + << "Async initialization did not start."; + + // Allow the init to complete. + init_can_complete.set_value(); +} + +// Test the asynchronous SetProvider for a named provider to ensure it doesn't +// block. +TEST_F(OpenFeatureAPITest, SetNamedProviderAsyncDoesNotBlock) { + std::shared_ptr mock_provider = + std::make_shared(); + std::string domain = "async-domain"; + std::promise init_can_start; + std::future init_started_future = init_can_start.get_future(); + std::promise init_can_complete; + std::future init_can_complete_future = init_can_complete.get_future(); + + EXPECT_CALL(*mock_provider, Init(_)).WillOnce([&](const auto&) { + init_can_start.set_value(); + init_can_complete_future.wait(); + return absl::OkStatus(); + }); + EXPECT_CALL(*mock_provider, Shutdown()).WillOnce(Return(absl::OkStatus())); + api.SetProvider(domain, mock_provider); + + // Confirm the background task has started. + auto status = init_started_future.wait_for(std::chrono::seconds(1)); + ASSERT_EQ(status, std::future_status::ready) + << "Async initialization did not start for named provider."; + + // Allow the init to complete. + init_can_complete.set_value(); +} + +// Test that GetClient returns a valid default ClientAPI instance. +TEST_F(OpenFeatureAPITest, GetDefaultClient) { + std::shared_ptr client = api.GetClient(); + EXPECT_NE(client, nullptr) << "GetClient() should return a valid ptr"; + EXPECT_EQ(client->GetMetadata().name, ""); +} + +// Test that GetClient returns a valid named ClientAPI instance. +TEST_F(OpenFeatureAPITest, GetNamedClient) { + std::shared_ptr named_client = api.GetClient("some-domain"); + EXPECT_NE(named_client, nullptr) + << "GetClient(domain) should return a valid ptr"; + EXPECT_EQ(named_client->GetMetadata().name, "some-domain"); +} + +// TODO: Add tests for "GetEvaluationContext" and "SetEvaluationContext" once. +// EvaluationContext logic is implemented. diff --git a/test/provider_repository_test.cpp b/test/provider_repository_test.cpp index 4f960f4..4be25b0 100644 --- a/test/provider_repository_test.cpp +++ b/test/provider_repository_test.cpp @@ -183,15 +183,24 @@ TEST_F(ProviderRepositoryTest, SetProviderWithFailedInitSetsErrorStatus) { EXPECT_EQ(repo.GetProviderStatus(), ProviderStatus::kError); } -// Test that getting status after shutdown does not crash and returns a safe -// value. -TEST_F(ProviderRepositoryTest, GetProviderStatusAfterShutdownReturnsNotReady) { - ASSERT_NE(repo.GetProvider(), nullptr); - ASSERT_EQ(repo.GetProviderStatus(), ProviderStatus::kReady); +// Test that Shutdown resets the repository to its initial +// state. +TEST_F(ProviderRepositoryTest, ShutdownResetsToReadyNoopProvider) { + // Set a mock provider to ensure we're not in the initial state. + auto mock_provider = std::make_shared(); + EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); + repo.SetProvider(mock_provider, ctx, true); + ASSERT_EQ(repo.GetProvider(), mock_provider); + // Expect the mock provider to be shut down. + EXPECT_CALL(*mock_provider, Shutdown()).WillOnce(Return(absl::OkStatus())); repo.Shutdown(); - EXPECT_EQ(repo.GetProviderStatus(), ProviderStatus::kNotReady); + // After shutdown, the repository should be reset to the default NoopProvider. + auto provider = repo.GetProvider(); + ASSERT_NE(provider, nullptr); + EXPECT_NE(dynamic_cast(provider.get()), nullptr); + EXPECT_EQ(repo.GetProviderStatus(), ProviderStatus::kReady); } // Test to verify the old provider is shutdown after a new one is ready. @@ -264,8 +273,24 @@ TEST_F(ProviderRepositoryTest, ShutdownAllProviders) { EXPECT_CALL(*mock_named_1, Shutdown()).WillOnce(Return(absl::OkStatus())); EXPECT_CALL(*mock_named_2, Shutdown()).WillOnce(Return(absl::OkStatus())); + // Keep a reference to the old manager. + std::shared_ptr old_manager = + repo.GetFeatureProviderStatusManager(); + repo.Shutdown(); - EXPECT_EQ(repo.GetFeatureProviderStatusManager(), nullptr); + + // Assert that a new default manager has been created and it's not the old + // one. + std::shared_ptr new_manager = + repo.GetFeatureProviderStatusManager(); + ASSERT_NE(new_manager, nullptr); + + ASSERT_NE(new_manager, old_manager); + + // Assert that the new provider is the default NoopProvider. + std::shared_ptr new_provider = new_manager->GetProvider(); + ASSERT_NE(new_provider, nullptr); + EXPECT_NE(dynamic_cast(new_provider.get()), nullptr); } // Test to verify that Shutdown waits for asynchronous initialization to finish.