diff --git a/.bazelversion b/.bazelversion index e0741a8..f9c71a5 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -8.5.1 \ No newline at end of file +8.5.1 diff --git a/openfeature/BUILD b/openfeature/BUILD index fa0e587..2c82f6e 100644 --- a/openfeature/BUILD +++ b/openfeature/BUILD @@ -16,6 +16,26 @@ cc_library( ], ) +cc_library( + name = "client_api", + srcs = ["client_api.cpp"], + hdrs = ["client_api.h"], + include_prefix = "openfeature", + deps = [ + ":client", + ":evaluation_context", + ":features", + ":flag_metadata", + ":global_context_manager", + ":metadata", + ":provider", + ":provider_repository", + ":provider_status", + ":reason", + ":resolution_details", + ], +) + cc_library( name = "error_code", hdrs = ["error_code.h"], diff --git a/openfeature/client_api.cpp b/openfeature/client_api.cpp new file mode 100644 index 0000000..93ca102 --- /dev/null +++ b/openfeature/client_api.cpp @@ -0,0 +1,71 @@ +#include "client_api.h" + +#include + +#include "openfeature/flag_metadata.h" +#include "openfeature/global_context_manager.h" +#include "openfeature/reason.h" + +namespace openfeature { + +ClientAPI::ClientAPI(ProviderRepository& repository, std::string_view domain) + : provider_repository_(repository), domain_(domain) {} + +Metadata ClientAPI::GetMetadata() { return Metadata{domain_}; } + +EvaluationContext ClientAPI::GetEvaluationContext() { + std::lock_guard lock(context_mutex_); + return evaluation_context_; +} + +void ClientAPI::SetEvaluationContext(const EvaluationContext& ctx) { + std::lock_guard lock(context_mutex_); + evaluation_context_ = ctx; +} + +ProviderStatus ClientAPI::GetProviderStatus() { + return provider_repository_.GetProviderStatus(domain_); +} + +bool ClientAPI::GetBooleanValue(std::string_view flag_key, bool default_value) { + return EvaluateBooleanFlag(flag_key, default_value, 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(); +} + +std::unique_ptr ClientAPI::EvaluateBooleanFlag( + std::string_view flag_key, bool default_value, + const std::optional& ctx) { + if (GetProviderStatus() != ProviderStatus::kReady) { + return std::make_unique( + default_value, Reason::kError, std::nullopt, FlagMetadata(), + ErrorCode::kProviderNotReady, "Provider is not ready"); + } + + std::shared_ptr provider = + provider_repository_.GetProvider(domain_); + if (!provider) { + return std::make_unique( + default_value, Reason::kError, std::nullopt, FlagMetadata(), + ErrorCode::kProviderFatal, "Provider not found for domain"); + } + + EvaluationContext merged_context = MergeContexts(ctx); + return provider->GetBooleanEvaluation(flag_key, default_value, + merged_context); +} + +EvaluationContext ClientAPI::MergeContexts( + const std::optional& invocation_ctx) { + // TODO: Add context merging logic after EvaluationContext is implemented. + + if (invocation_ctx) { + return *invocation_ctx; + } + return GetEvaluationContext(); +} + +} // namespace openfeature \ No newline at end of file diff --git a/openfeature/client_api.h b/openfeature/client_api.h new file mode 100644 index 0000000..4a3d1ed --- /dev/null +++ b/openfeature/client_api.h @@ -0,0 +1,69 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_CLIENT_API_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_CLIENT_API_H_ + +#include +#include +#include +#include +#include + +#include "openfeature/client.h" +#include "openfeature/evaluation_context.h" +#include "openfeature/features.h" +#include "openfeature/global_context_manager.h" +#include "openfeature/metadata.h" +#include "openfeature/provider.h" +#include "openfeature/provider_repository.h" +#include "openfeature/provider_status.h" +#include "openfeature/resolution_details.h" + +namespace openfeature { + +// OpenFeature client implementation. +class ClientAPI : public Client { + public: + ClientAPI(ProviderRepository& repository, std::string_view domain); + + ~ClientAPI() override = default; + + ClientAPI(const ClientAPI&) = delete; + ClientAPI& operator=(const ClientAPI&) = delete; + + Metadata GetMetadata() override; + + // Return an optional client-level evaluation context. + EvaluationContext GetEvaluationContext() override; + + // Set the client-level evaluation context. + void SetEvaluationContext(const EvaluationContext& ctx) override; + + // Returns the current status of the associated provider. + ProviderStatus GetProviderStatus() override; + + 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; + + // TODO: Add methods to get and set Hooks. + // TODO: Add methods for flag evaluation for other types (e.g. string, int, + // float, object). + // TODO: Add methods for detailed flag evaluation. + // TODO: Overload method "GetBooleanValue" to accept "Evaluation Options". + + private: + std::unique_ptr EvaluateBooleanFlag( + std::string_view flag_key, bool default_value, + const std::optional& ctx); + + EvaluationContext MergeContexts( + const std::optional& invocation_ctx); + + ProviderRepository& provider_repository_; + std::string domain_; + EvaluationContext evaluation_context_; + mutable std::mutex context_mutex_; +}; + +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_CLIENT_API_H_ diff --git a/test/BUILD b/test/BUILD index 3d38a78..620c958 100644 --- a/test/BUILD +++ b/test/BUILD @@ -11,6 +11,16 @@ cc_library( ], ) +cc_test( + name = "client_api_test", + srcs = ["client_api_test.cpp"], + deps = [ + ":mock_feature_provider", + "//openfeature:client_api", + "@googletest//:gtest_main", + ], +) + cc_test( name = "feature_provider_status_manager_test", srcs = ["feature_provider_status_manager_test.cpp"], diff --git a/test/client_api_test.cpp b/test/client_api_test.cpp new file mode 100644 index 0000000..8c5039a --- /dev/null +++ b/test/client_api_test.cpp @@ -0,0 +1,103 @@ +#include "openfeature/client_api.h" + +#include +#include + +#include +#include + +#include "absl/status/status.h" +#include "mocks/mock_feature_provider.h" +#include "openfeature/evaluation_context.h" +#include "openfeature/global_context_manager.h" +#include "openfeature/provider_status.h" + +using namespace openfeature; +using ::testing::_; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::StrictMock; + +class ClientAPITest : public ::testing::Test { + protected: + void SetUp() override { + // Reset the Global Context to a clean state before each test. + GlobalContextManager::GetInstance().SetGlobalEvaluationContext( + EvaluationContext{}); + } + ProviderRepository repo_; +}; + +// Test that the constructor correctly sets the domain in the metadata. +TEST_F(ClientAPITest, ConstructorSetsDomainMetadata) { + std::string domain = "test-domain"; + ClientAPI client(repo_, domain); + + Metadata metadata = client.GetMetadata(); + EXPECT_EQ(metadata.name, domain); +} + +// Test that the provider status is Ready by default. +TEST_F(ClientAPITest, GetProviderStatusDefaultsToReady) { + ClientAPI client(repo_, "test-domain"); + EXPECT_EQ(client.GetProviderStatus(), ProviderStatus::kReady); +} + +// Test setting and getting the EvaluationContext. +TEST_F(ClientAPITest, SetAndGetEvaluationContext) { + ClientAPI client(repo_, "test-domain"); + EvaluationContext ctx; + + // Verify we can set the context without error. + EXPECT_NO_THROW(client.SetEvaluationContext(ctx)); +} + +// Test that GetBooleanValue returns the default value when using the default +// provider. +TEST_F(ClientAPITest, GetBooleanValueReturnsDefaultWithNoopProvider) { + ClientAPI client(repo_, "test-domain"); + std::string flag_key = "my-boolean-flag"; + + EXPECT_TRUE(client.GetBooleanValue(flag_key, true)); + + EXPECT_FALSE(client.GetBooleanValue(flag_key, false)); +} + +// Test GetBooleanValue with an EvaluationContext passed in. +TEST_F(ClientAPITest, GetBooleanValueWithContextReturnsDefault) { + ClientAPI client(repo_, "test-domain"); + EvaluationContext ctx; + std::string flag_key = "my-boolean-flag"; + + EXPECT_TRUE(client.GetBooleanValue(flag_key, true, ctx)); + EXPECT_FALSE(client.GetBooleanValue(flag_key, false, ctx)); +} + +// Test context merging logic indirectly. +TEST_F(ClientAPITest, GetBooleanValueSafeWithMergedContexts) { + EvaluationContext global_ctx; + GlobalContextManager::GetInstance().SetGlobalEvaluationContext(global_ctx); + + ClientAPI client(repo_, "test-domain"); + EvaluationContext client_ctx; + client.SetEvaluationContext(client_ctx); + + EvaluationContext invocation_ctx; + + // This call forces a merge of Global + Client + Invocation contexts. + // We expect the NoopProvider to handle the result gracefully (return + // default). + EXPECT_TRUE(client.GetBooleanValue("flag", true, invocation_ctx)); +} + +// Test behavior when the domain is empty. +TEST_F(ClientAPITest, WorksWithEmptyDomain) { + ClientAPI client(repo_, ""); + EXPECT_EQ(client.GetMetadata().name, ""); + EXPECT_TRUE(client.GetBooleanValue("flag", true)); +} + +// TODO: If ClientAPI is refactored to allow injecting a MockFeatureProvider +// (e.g. by passing the OpenFeatureAPI's repository or via constructor), +// add tests here to verify that the MockProvider receives the correct +// flag key and merged EvaluationContext. \ No newline at end of file