From ef13958cbc47b3aa9148f1f4fd99eec2ba25202e Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Thu, 5 Feb 2026 14:28:55 +0000 Subject: [PATCH 1/4] Update SDK to allow evaluation for more than just boolean flags. Signed-off-by: NeaguGeorgiana23 --- openfeature/BUILD | 17 ++++ openfeature/client_api.cpp | 114 ++++++++++++++++++--- openfeature/client_api.h | 71 ++++++++++++- openfeature/features.h | 25 ++++- openfeature/noop_provider.cpp | 30 ++++++ openfeature/noop_provider.h | 20 +++- openfeature/provider.h | 15 ++- openfeature/resolution_details.cpp | 2 +- openfeature/resolution_details.h | 3 +- openfeature/value.cpp | 158 +++++++++++++++++++++++++++++ openfeature/value.h | 71 +++++++++++++ test/client_api_test.cpp | 64 ++++++++++++ test/mocks/mock_feature_provider.h | 17 +++- test/noop_provider_test.cpp | 93 ++++++++++++++++- test/resolution_details_test.cpp | 92 ++++++++++++++++- 15 files changed, 758 insertions(+), 34 deletions(-) create mode 100644 openfeature/value.cpp create mode 100644 openfeature/value.h diff --git a/openfeature/BUILD b/openfeature/BUILD index fc98289..9acee9a 100644 --- a/openfeature/BUILD +++ b/openfeature/BUILD @@ -33,6 +33,7 @@ cc_library( ":provider_status", ":reason", ":resolution_details", + ":value", ], ) @@ -69,6 +70,7 @@ cc_library( include_prefix = "openfeature", deps = [ ":evaluation_context", + ":value" ], ) @@ -121,6 +123,7 @@ cc_library( ":metadata", ":provider", ":resolution_details", + ":value", ], ) @@ -165,6 +168,7 @@ cc_library( ":evaluation_context", ":metadata", ":resolution_details", + ":value", ], ) @@ -179,6 +183,19 @@ cc_library( srcs = ["resolution_details.cpp"], hdrs = ["resolution_details.h"], include_prefix = "openfeature", + deps = [ + ":error_code", + ":flag_metadata", + ":reason", + ":value", + ], +) + +cc_library( + name = "value", + srcs = ["value.cpp"], + hdrs = ["value.h"], + include_prefix = "openfeature", deps = [ ":error_code", ":flag_metadata", diff --git a/openfeature/client_api.cpp b/openfeature/client_api.cpp index 9b654b3..af404b6 100644 --- a/openfeature/client_api.cpp +++ b/openfeature/client_api.cpp @@ -38,26 +38,108 @@ bool ClientAPI::GetBooleanValue(std::string_view flag_key, bool default_value, return EvaluateBooleanFlag(flag_key, default_value, ctx)->GetValue(); } +std::string ClientAPI::GetStringValue(std::string_view flag_key, + std::string_view default_value) { + return EvaluateStringFlag(flag_key, default_value, 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(); +} + +int64_t ClientAPI::GetIntegerValue(std::string_view flag_key, + int64_t default_value) { + return EvaluateIntegerFlag(flag_key, default_value, 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(); +} + +double ClientAPI::GetDoubleValue(std::string_view flag_key, + double default_value) { + return EvaluateDoubleFlag(flag_key, default_value, 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(); +} + +Value ClientAPI::GetObjectValue(std::string_view flag_key, + Value default_value) { + return EvaluateObjectFlag(flag_key, default_value, 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(); +} + 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"); - } + return this->EvaluateFlag( + default_value, ctx, + [&](const std::shared_ptr& provider, + const EvaluationContext& merged_ctx) { + return provider->GetBooleanEvaluation(flag_key, default_value, + merged_ctx); + }); +} - 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"); - } +std::unique_ptr ClientAPI::EvaluateStringFlag( + std::string_view flag_key, std::string_view default_value, + const std::optional& ctx) { + std::string default_str(default_value); + return this->EvaluateFlag( + default_str, ctx, + [&](const std::shared_ptr& provider, + const EvaluationContext& merged_ctx) { + return provider->GetStringEvaluation(flag_key, default_value, + merged_ctx); + }); +} - EvaluationContext merged_context = MergeContexts(ctx); - return provider->GetBooleanEvaluation(flag_key, default_value, - merged_context); +std::unique_ptr ClientAPI::EvaluateIntegerFlag( + std::string_view flag_key, int64_t default_value, + const std::optional& ctx) { + return this->EvaluateFlag( + default_value, ctx, + [&](const std::shared_ptr& provider, + const EvaluationContext& merged_ctx) { + return provider->GetIntegerEvaluation(flag_key, default_value, + merged_ctx); + }); +} + +std::unique_ptr ClientAPI::EvaluateDoubleFlag( + std::string_view flag_key, double default_value, + const std::optional& ctx) { + return this->EvaluateFlag( + default_value, ctx, + [&](const std::shared_ptr& provider, + const EvaluationContext& merged_ctx) { + return provider->GetDoubleEvaluation(flag_key, default_value, + merged_ctx); + }); +} + +std::unique_ptr ClientAPI::EvaluateObjectFlag( + std::string_view flag_key, Value default_value, + const std::optional& ctx) { + return this->EvaluateFlag( + default_value, ctx, + [&](const std::shared_ptr& provider, + const EvaluationContext& merged_ctx) { + return provider->GetObjectEvaluation(flag_key, default_value, + merged_ctx); + }); } EvaluationContext ClientAPI::MergeContexts( @@ -70,4 +152,4 @@ EvaluationContext ClientAPI::MergeContexts( return GetEvaluationContext(); } -} // namespace openfeature \ No newline at end of file +} // namespace openfeature diff --git a/openfeature/client_api.h b/openfeature/client_api.h index 4a3d1ed..cb4f338 100644 --- a/openfeature/client_api.h +++ b/openfeature/client_api.h @@ -16,6 +16,7 @@ #include "openfeature/provider_repository.h" #include "openfeature/provider_status.h" #include "openfeature/resolution_details.h" +#include "openfeature/value.h" namespace openfeature { @@ -40,21 +41,64 @@ class ClientAPI : public Client { // Returns the current status of the associated provider. ProviderStatus GetProviderStatus() override; + // Evaluate a boolean flag. 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; + // Evaluate a string flag. + std::string GetStringValue(std::string_view flag_key, + std::string_view default_value) override; + std::string GetStringValue(std::string_view flag_key, + std::string_view default_value, + const EvaluationContext& ctx) 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; + // 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; + // 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; + // 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: + template + std::unique_ptr EvaluateFlag( + ValueType default_value, const std::optional& ctx, + ProviderCallable provider_call); + std::unique_ptr EvaluateBooleanFlag( std::string_view flag_key, bool default_value, const std::optional& ctx); + std::unique_ptr EvaluateStringFlag( + std::string_view flag_key, std::string_view default_value, + const std::optional& ctx); + + std::unique_ptr EvaluateIntegerFlag( + std::string_view flag_key, int64_t default_value, + const std::optional& ctx); + + std::unique_ptr EvaluateDoubleFlag( + std::string_view flag_key, double default_value, + const std::optional& ctx); + + std::unique_ptr EvaluateObjectFlag( + std::string_view flag_key, Value default_value, + const std::optional& ctx); + EvaluationContext MergeContexts( const std::optional& invocation_ctx); @@ -64,6 +108,29 @@ class ClientAPI : public Client { mutable std::mutex context_mutex_; }; +template +std::unique_ptr ClientAPI::EvaluateFlag( + ValueType default_value, const std::optional& ctx, + ProviderCallable provider_call) { + 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_call(provider, merged_context); +} + } // namespace openfeature #endif // CPP_SDK_INCLUDE_OPENFEATURE_CLIENT_API_H_ diff --git a/openfeature/features.h b/openfeature/features.h index a123544..83897eb 100644 --- a/openfeature/features.h +++ b/openfeature/features.h @@ -4,18 +4,41 @@ #include #include "openfeature/evaluation_context.h" +#include "openfeature/value.h" namespace openfeature { class Features { public: virtual ~Features() = default; + virtual bool GetBooleanValue(std::string_view flag_key, bool default_value) = 0; virtual bool GetBooleanValue(std::string_view flag_key, bool default_value, const EvaluationContext& ctx) = 0; - // TODO: Add other flag types (e.g. string, int, float, object) + 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 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 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 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 }; diff --git a/openfeature/noop_provider.cpp b/openfeature/noop_provider.cpp index 1238397..b5b617b 100644 --- a/openfeature/noop_provider.cpp +++ b/openfeature/noop_provider.cpp @@ -11,4 +11,34 @@ std::unique_ptr NoopProvider::GetBooleanEvaluation( std::nullopt, ""); } +std::unique_ptr NoopProvider::GetStringEvaluation( + std::string_view flag, std::string_view default_value, + const EvaluationContext& ctx) { + return std::make_unique( + std::string(default_value), Reason::kDefault, "default-variant", + FlagMetadata(), std::nullopt, ""); +} + +std::unique_ptr NoopProvider::GetIntegerEvaluation( + std::string_view flag, int64_t default_value, + const EvaluationContext& ctx) { + return std::make_unique( + default_value, Reason::kDefault, "default-variant", FlagMetadata(), + std::nullopt, ""); +} + +std::unique_ptr NoopProvider::GetDoubleEvaluation( + std::string_view flag, double default_value, const EvaluationContext& ctx) { + return std::make_unique( + default_value, Reason::kDefault, "default-variant", FlagMetadata(), + std::nullopt, ""); +} + +std::unique_ptr NoopProvider::GetObjectEvaluation( + std::string_view flag, Value default_value, const EvaluationContext& ctx) { + return std::make_unique( + default_value, Reason::kDefault, "default-variant", FlagMetadata(), + std::nullopt, ""); +} + } // namespace openfeature diff --git a/openfeature/noop_provider.h b/openfeature/noop_provider.h index 420abf1..d15e408 100644 --- a/openfeature/noop_provider.h +++ b/openfeature/noop_provider.h @@ -9,6 +9,7 @@ #include "openfeature/metadata.h" #include "openfeature/provider.h" #include "openfeature/resolution_details.h" +#include "openfeature/value.h" namespace openfeature { @@ -26,7 +27,24 @@ class NoopProvider : public FeatureProvider { std::string_view flag, bool default_value, const EvaluationContext& ctx) override; - // TODO: Add other flag types (e.g. string, int, float, object) + // StringResolutionDetails returns a string flag. + std::unique_ptr GetStringEvaluation( + std::string_view flag, std::string_view default_value, + const EvaluationContext& ctx); + + // IntResolutionDetails returns an integer flag. + std::unique_ptr GetIntegerEvaluation( + std::string_view flag, int64_t default_value, + const EvaluationContext& ctx); + + // DoubleResolutionDetails returns a double flag. + std::unique_ptr GetDoubleEvaluation( + std::string_view flag, double default_value, + const EvaluationContext& ctx); + + // ObjectResolutionDetails returns an object flag. + std::unique_ptr GetObjectEvaluation( + std::string_view flag, Value default_value, const EvaluationContext& ctx); private: std::string name_ = "Noop Provider"; diff --git a/openfeature/provider.h b/openfeature/provider.h index 5e5e3db..3324e03 100644 --- a/openfeature/provider.h +++ b/openfeature/provider.h @@ -8,6 +8,7 @@ #include "openfeature/evaluation_context.h" #include "openfeature/metadata.h" #include "openfeature/resolution_details.h" +#include "openfeature/value.h" namespace openfeature { @@ -22,12 +23,22 @@ class FeatureProvider { virtual std::unique_ptr GetBooleanEvaluation( std::string_view flag, bool default_value, const EvaluationContext& ctx) = 0; + virtual std::unique_ptr GetStringEvaluation( + std::string_view flag, std::string_view default_value, + const EvaluationContext& ctx) = 0; + virtual std::unique_ptr GetIntegerEvaluation( + std::string_view flag, int64_t default_value, + const EvaluationContext& ctx) = 0; + virtual std::unique_ptr GetDoubleEvaluation( + std::string_view flag, double default_value, + const EvaluationContext& ctx) = 0; + virtual std::unique_ptr GetObjectEvaluation( + std::string_view flag, Value default_value, + const EvaluationContext& ctx) = 0; virtual absl::Status Init(const EvaluationContext& ctx) { return absl::OkStatus(); } virtual absl::Status Shutdown() { return absl::OkStatus(); } - - // TODO: Add other flag types (e.g. string, int, float, object) }; } // namespace openfeature diff --git a/openfeature/resolution_details.cpp b/openfeature/resolution_details.cpp index 32185db..b6fd95c 100644 --- a/openfeature/resolution_details.cpp +++ b/openfeature/resolution_details.cpp @@ -48,6 +48,6 @@ template class ResolutionDetails; template class ResolutionDetails; template class ResolutionDetails; template class ResolutionDetails; -template class ResolutionDetails; +template class ResolutionDetails; } // namespace openfeature \ No newline at end of file diff --git a/openfeature/resolution_details.h b/openfeature/resolution_details.h index 1a1cd57..c8976e2 100644 --- a/openfeature/resolution_details.h +++ b/openfeature/resolution_details.h @@ -8,6 +8,7 @@ #include "openfeature/error_code.h" #include "openfeature/flag_metadata.h" #include "openfeature/reason.h" +#include "openfeature/value.h" namespace openfeature { @@ -44,7 +45,7 @@ using BoolResolutionDetails = ResolutionDetails; using StringResolutionDetails = ResolutionDetails; using IntResolutionDetails = ResolutionDetails; using DoubleResolutionDetails = ResolutionDetails; -using ObjectResolutionDetails = ResolutionDetails; +using ObjectResolutionDetails = ResolutionDetails; } // namespace openfeature diff --git a/openfeature/value.cpp b/openfeature/value.cpp new file mode 100644 index 0000000..9b3b296 --- /dev/null +++ b/openfeature/value.cpp @@ -0,0 +1,158 @@ +#include "openfeature/value.h" + +#include + +namespace openfeature { + +Value::Value() : inner_value_(std::monostate{}) {} + +Value::Value(bool value) : inner_value_(value) {} + +Value::Value(int64_t value) : inner_value_(value) {} +Value::Value(int value) : inner_value_(static_cast(value)) {} + +Value::Value(double value) : inner_value_(value) {} + +Value::Value(std::string value) : inner_value_(std::move(value)) {} + +Value::Value(const char* value) : inner_value_(std::string(value)) {} + +Value::Value(const std::map& value) + : inner_value_(std::make_unique>(value)) {} + +Value::Value(const std::vector& value) + : inner_value_(std::make_unique>(value)) {} + +Value::Value(std::chrono::system_clock::time_point value) + : inner_value_(value) {} + +Value::Value(const Value& other) { + if (other.IsStructure()) { + auto& ptr = std::get>>( + other.inner_value_); + inner_value_ = + ptr ? std::make_unique>(*ptr) : nullptr; + } else if (other.IsList()) { + auto& ptr = + std::get>>(other.inner_value_); + inner_value_ = ptr ? std::make_unique>(*ptr) : nullptr; + } else { + std::visit( + [this](auto&& arg) { + using T = std::decay_t; + if constexpr (!std::is_same_v< + T, std::unique_ptr>> && + !std::is_same_v>>) { + this->inner_value_ = arg; + } + }, + other.inner_value_); + } +} + +Value& Value::operator=(const Value& other) { + if (this == &other) return *this; + Value temp(other); + std::swap(this->inner_value_, temp.inner_value_); + return *this; +} + +bool Value::IsNull() const { + return std::holds_alternative(inner_value_); +} +bool Value::IsBool() const { + return std::holds_alternative(inner_value_); +} +bool Value::IsNumber() const { + return std::holds_alternative(inner_value_) || + std::holds_alternative(inner_value_); +} +bool Value::IsString() const { + return std::holds_alternative(inner_value_); +} +bool Value::IsStructure() const { + return std::holds_alternative>>( + inner_value_); +} +bool Value::IsList() const { + return std::holds_alternative>>( + inner_value_); +} +bool Value::IsDateTime() const { + return std::holds_alternative( + inner_value_); +} + +std::optional Value::AsBool() const { + if (auto* v = std::get_if(&inner_value_)) return *v; + return std::nullopt; +} + +std::optional Value::AsString() const { + if (auto* v = std::get_if(&inner_value_)) return *v; + return std::nullopt; +} + +std::optional Value::AsInt() const { + if (auto* v = std::get_if(&inner_value_)) return *v; + if (auto* v = std::get_if(&inner_value_)) + return static_cast(std::round(*v)); + return std::nullopt; +} + +std::optional Value::AsDouble() const { + if (auto* v = std::get_if(&inner_value_)) return *v; + if (auto* v = std::get_if(&inner_value_)) + return static_cast(*v); + return std::nullopt; +} + +std::optional Value::AsDateTime() const { + if (auto* v = + std::get_if(&inner_value_)) + return *v; + return std::nullopt; +} + +const std::map* Value::AsStructure() const { + if (auto* v = std::get_if>>( + &inner_value_)) { + return v->get(); + } + return nullptr; +} + +const std::vector* Value::AsList() const { + if (auto* v = + std::get_if>>(&inner_value_)) { + return v->get(); + } + return nullptr; +} + +bool operator==(const Value& lhs, const Value& rhs) { + if (lhs.IsBool() && rhs.IsBool()) return lhs.AsBool() == rhs.AsBool(); + + if (lhs.IsString() && rhs.IsString()) return lhs.AsString() == rhs.AsString(); + + if (lhs.IsNumber() && rhs.IsNumber()) return lhs.AsDouble() == rhs.AsDouble(); + + if (lhs.IsNull() && rhs.IsNull()) return true; + + if (lhs.IsDateTime() && rhs.IsDateTime()) + return lhs.AsDateTime() == rhs.AsDateTime(); + + if (lhs.IsStructure() && rhs.IsStructure()) { + return *lhs.AsStructure() == *rhs.AsStructure(); + } + if (lhs.IsList() && rhs.IsList()) { + return *lhs.AsList() == *rhs.AsList(); + } + + return false; +} + +bool operator!=(const Value& lhs, const Value& rhs) { return !(lhs == rhs); } + +} // namespace openfeature \ No newline at end of file diff --git a/openfeature/value.h b/openfeature/value.h new file mode 100644 index 0000000..4ecc9d2 --- /dev/null +++ b/openfeature/value.h @@ -0,0 +1,71 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_VALUE_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_VALUE_H_ + +#include +#include +#include +#include +#include +#include +#include + +namespace openfeature { + +// Value serves as a generic return type for structured data from providers. +// It can hold Null, Boolean, String, Integer, Double, Structure (Map), List, or +// DateTime. +class Value { + public: + Value(); + Value(bool value); + Value(int64_t value); + Value(int value); + Value(double value); + Value(std::string value); + Value(const char* value); + Value(const std::map& value); + Value(const std::vector& value); + Value(std::chrono::system_clock::time_point value); + + Value(const Value& other); + Value& operator=(const Value& other); + Value(Value&& other) noexcept = default; + Value& operator=(Value&& other) noexcept = default; + ~Value() = default; + + bool IsNull() const; + bool IsBool() const; + bool IsNumber() const; + bool IsString() const; + bool IsStructure() const; + bool IsList() const; + bool IsDateTime() const; + + // Returns generic optional, or nullopt if type mismatch + std::optional AsBool() const; + std::optional AsString() const; + std::optional AsInt() const; + std::optional AsDouble() const; + std::optional AsDateTime() const; + + // For complex types, return pointers to avoid expensive copies on access + // Returns nullptr if type mismatch + const std::map* AsStructure() const; + const std::vector* AsList() const; + + private: + using InternalVariant = + std::variant>, + std::unique_ptr>>; + + InternalVariant inner_value_; +}; + +bool operator==(const Value& lhs, const Value& rhs); +bool operator!=(const Value& lhs, const Value& rhs); + +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_VALUE_H_ diff --git a/test/client_api_test.cpp b/test/client_api_test.cpp index 94731f6..a14278a 100644 --- a/test/client_api_test.cpp +++ b/test/client_api_test.cpp @@ -63,6 +63,38 @@ TEST_F(ClientAPITest, GetBooleanValueReturnsDefaultWithNoopProvider) { EXPECT_FALSE(client.GetBooleanValue(flag_key, false)); } +// Test that GetStringValue returns the default value when using the default +// provider. +TEST_F(ClientAPITest, GetStringValueReturnsDefaultWithNoopProvider) { + ClientAPI client(repo_, "test-domain"); + std::string flag_key = "my-string-flag"; + EXPECT_EQ(client.GetStringValue(flag_key, "default"), "default"); +} + +// Test that GetIntegerValue returns the default value when using the default +// provider. +TEST_F(ClientAPITest, GetIntegerValueReturnsDefaultWithNoopProvider) { + ClientAPI client(repo_, "test-domain"); + std::string flag_key = "my-integer-flag"; + EXPECT_EQ(client.GetIntegerValue(flag_key, 42), 42); +} + +// Test that GetDoubleValue returns the default value when using the default +// provider. +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); +} + +// Test that GetObjectValue returns the default value when using the default +// provider. +TEST_F(ClientAPITest, GetObjectValueReturnsDefaultWithNoopProvider) { + ClientAPI client(repo_, "test-domain"); + std::string flag_key = "my-object-flag"; + EXPECT_EQ(client.GetObjectValue(flag_key, Value(1)), Value(1)); +} + // Test GetBooleanValue with an EvaluationContext passed in. TEST_F(ClientAPITest, GetBooleanValueWithContextReturnsDefault) { ClientAPI client(repo_, "test-domain"); @@ -73,6 +105,38 @@ TEST_F(ClientAPITest, GetBooleanValueWithContextReturnsDefault) { EXPECT_FALSE(client.GetBooleanValue(flag_key, false, ctx)); } +// Test GetStringValue with an EvaluationContext passed in. +TEST_F(ClientAPITest, GetStringValueWithContextReturnsDefault) { + ClientAPI client(repo_, "test-domain"); + EvaluationContext ctx = EvaluationContext::Builder().build(); + std::string flag_key = "my-string-flag"; + EXPECT_EQ(client.GetStringValue(flag_key, "default", ctx), "default"); +} + +// Test that GetIntegerValue with an EvaluationContext passed in. +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); +} + +// Test that GetDoubleValue with an EvaluationContext passed in. +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); +} + +// Test that GetObjectValue with an EvaluationContext passed in. +TEST_F(ClientAPITest, GetObjectValueWithContextReturnsDefault) { + ClientAPI client(repo_, "test-domain"); + EvaluationContext ctx = EvaluationContext::Builder().build(); + std::string flag_key = "my-object-flag"; + EXPECT_EQ(client.GetObjectValue(flag_key, Value(1), ctx), Value(1)); +} + // Test context merging logic indirectly. TEST_F(ClientAPITest, GetBooleanValueSafeWithMergedContexts) { EvaluationContext global_ctx = EvaluationContext::Builder().build(); diff --git a/test/mocks/mock_feature_provider.h b/test/mocks/mock_feature_provider.h index 6ed49ae..67e7f78 100644 --- a/test/mocks/mock_feature_provider.h +++ b/test/mocks/mock_feature_provider.h @@ -15,7 +15,22 @@ class MockFeatureProvider : public FeatureProvider { (std::string_view flag, bool default_value, const EvaluationContext& ctx), (override)); - + MOCK_METHOD(std::unique_ptr, GetStringEvaluation, + (std::string_view flag, std::string_view default_value, + const EvaluationContext& ctx), + (override)); + MOCK_METHOD(std::unique_ptr, GetIntegerEvaluation, + (std::string_view flag, int64_t default_value, + const EvaluationContext& ctx), + (override)); + MOCK_METHOD(std::unique_ptr, GetDoubleEvaluation, + (std::string_view flag, double default_value, + const EvaluationContext& ctx), + (override)); + MOCK_METHOD(std::unique_ptr, GetObjectEvaluation, + (std::string_view flag, Value default_value, + const EvaluationContext& ctx), + (override)); MOCK_METHOD(absl::Status, Init, (const EvaluationContext& ctx), (override)); MOCK_METHOD(absl::Status, Shutdown, (), (override)); diff --git a/test/noop_provider_test.cpp b/test/noop_provider_test.cpp index e3e880e..aa15187 100644 --- a/test/noop_provider_test.cpp +++ b/test/noop_provider_test.cpp @@ -30,10 +30,10 @@ TEST_F(NoopProviderTest, ShutdownShouldReturnOkStatus) { EXPECT_EQ(status, absl::OkStatus()); } +// Test to verify the boolean evaluation returns the default value. class NoopProviderBooleanTest : public NoopProviderTest, public ::testing::WithParamInterface {}; -// Test to verify the boolean evaluation returns the default value. TEST_P(NoopProviderBooleanTest, BooleanEvaluationShouldReturnDefaultValue) { const bool defaultValue = GetParam(); @@ -51,8 +51,91 @@ TEST_P(NoopProviderBooleanTest, BooleanEvaluationShouldReturnDefaultValue) { INSTANTIATE_TEST_SUITE_P(BooleanDefaultValues, NoopProviderBooleanTest, testing::Values(true, false)); -// Main function to initialize and run all tests. -int main(int argc, char** argv) { - testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); +class NoopProviderStringTest + : public NoopProviderTest, + public ::testing::WithParamInterface {}; + +// Test to verify the string evaluation returns the default value. +TEST_P(NoopProviderStringTest, StringEvaluationShouldReturnDefaultValue) { + const std::string defaultValue = GetParam(); + + const std::unique_ptr details = + provider_.GetStringEvaluation("my-string-flag", defaultValue, ctx_); + + EXPECT_EQ(details->GetValue(), defaultValue); + EXPECT_EQ(details->GetReason(), Reason::kDefault); + EXPECT_EQ(details->GetVariant(), "default-variant"); + EXPECT_FALSE(details->GetErrorCode().has_value()); + ASSERT_TRUE(details->GetErrorMessage().has_value()); + EXPECT_TRUE(details->GetErrorMessage()->empty()); +} + +INSTANTIATE_TEST_SUITE_P(StringDefaultValues, NoopProviderStringTest, + testing::Values("hello", "world", "")); + +class NoopProviderIntegerTest : public NoopProviderTest, + public ::testing::WithParamInterface { +}; + +// Test to verify the integer evaluation returns the default value. +TEST_P(NoopProviderIntegerTest, IntegerEvaluationShouldReturnDefaultValue) { + const int64_t defaultValue = GetParam(); + + const std::unique_ptr details = + provider_.GetIntegerEvaluation("my-int-flag", defaultValue, ctx_); + + EXPECT_EQ(details->GetValue(), defaultValue); + EXPECT_EQ(details->GetReason(), Reason::kDefault); + EXPECT_EQ(details->GetVariant(), "default-variant"); + EXPECT_FALSE(details->GetErrorCode().has_value()); + ASSERT_TRUE(details->GetErrorMessage().has_value()); + EXPECT_TRUE(details->GetErrorMessage()->empty()); +} + +INSTANTIATE_TEST_SUITE_P(IntegerDefaultValues, NoopProviderIntegerTest, + testing::Values(100, -42, 0, 9223372036854775807LL)); + +class NoopProviderDoubleTest : public NoopProviderTest, + public ::testing::WithParamInterface {}; + +// Test to verify the double evaluation returns the default value. +TEST_P(NoopProviderDoubleTest, DoubleEvaluationShouldReturnDefaultValue) { + const double defaultValue = GetParam(); + + const std::unique_ptr details = + provider_.GetDoubleEvaluation("my-double-flag", defaultValue, ctx_); + + EXPECT_DOUBLE_EQ(details->GetValue(), defaultValue); + EXPECT_EQ(details->GetReason(), Reason::kDefault); + EXPECT_EQ(details->GetVariant(), "default-variant"); + EXPECT_FALSE(details->GetErrorCode().has_value()); + ASSERT_TRUE(details->GetErrorMessage().has_value()); + EXPECT_TRUE(details->GetErrorMessage()->empty()); +} + +INSTANTIATE_TEST_SUITE_P(DoubleDefaultValues, NoopProviderDoubleTest, + testing::Values(3.14, -100.5, 0.0)); + +TEST_F(NoopProviderTest, ObjectEvaluationShouldReturnDefaultValue) { + std::map inner_struct; + inner_struct["inner_key"] = Value("inner_value"); + + std::map default_struct; + default_struct["a_bool"] = Value(true); + default_struct["an_int"] = Value(123); + default_struct["a_list"] = + Value(std::vector{{Value("item1"), Value(1.23)}}); + default_struct["a_struct"] = Value(inner_struct); + + const Value defaultValue(default_struct); + + const std::unique_ptr details = + provider_.GetObjectEvaluation("my-object-flag", defaultValue, ctx_); + + EXPECT_EQ(details->GetValue(), defaultValue); + EXPECT_EQ(details->GetReason(), Reason::kDefault); + EXPECT_EQ(details->GetVariant(), "default-variant"); + EXPECT_FALSE(details->GetErrorCode().has_value()); + ASSERT_TRUE(details->GetErrorMessage().has_value()); + EXPECT_TRUE(details->GetErrorMessage()->empty()); } diff --git a/test/resolution_details_test.cpp b/test/resolution_details_test.cpp index 5391044..fc42e49 100644 --- a/test/resolution_details_test.cpp +++ b/test/resolution_details_test.cpp @@ -6,7 +6,7 @@ using namespace openfeature; -TEST(ResolutionDetailsTest, AccessesFieldsAfterInitialization) { +TEST(ResolutionDetailsTest, AccessesFieldsAfterInitializationForBoolean) { const bool expected_value = true; const Reason expected_reason = Reason::kTargetingMatch; const std::optional expected_variant = "on-variant"; @@ -28,7 +28,91 @@ TEST(ResolutionDetailsTest, AccessesFieldsAfterInitialization) { ASSERT_NO_THROW(details.GetFlagMetadata()); } -int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); +TEST(ResolutionDetailsTest, AccessesFieldsAfterInitializationForString) { + const std::string expected_value = "expected-string"; + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "on-variant"; + const std::optional expected_error_code = ErrorCode::kParseError; + const std::optional expected_error_message = + "Failed to parse data"; + + const FlagMetadata expected_flag_metadata{}; + + StringResolutionDetails details(expected_value, expected_reason, + expected_variant, expected_flag_metadata, + expected_error_code, expected_error_message); + + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), expected_error_code); + ASSERT_EQ(details.GetErrorMessage(), expected_error_message); + ASSERT_NO_THROW(details.GetFlagMetadata()); +} + +TEST(ResolutionDetailsTest, AccessesFieldsAfterInitializationForInteger) { + const int64_t expected_value = 123; + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "on-variant"; + const std::optional expected_error_code = ErrorCode::kParseError; + const std::optional expected_error_message = + "Failed to parse data"; + + const FlagMetadata expected_flag_metadata{}; + + IntResolutionDetails details(expected_value, expected_reason, + expected_variant, expected_flag_metadata, + expected_error_code, expected_error_message); + + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), expected_error_code); + ASSERT_EQ(details.GetErrorMessage(), expected_error_message); + ASSERT_NO_THROW(details.GetFlagMetadata()); +} + +TEST(ResolutionDetailsTest, AccessesFieldsAfterInitializationForDouble) { + const double expected_value = 123.456; + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "on-variant"; + const std::optional expected_error_code = ErrorCode::kParseError; + const std::optional expected_error_message = + "Failed to parse data"; + + const FlagMetadata expected_flag_metadata{}; + + DoubleResolutionDetails details(expected_value, expected_reason, + expected_variant, expected_flag_metadata, + expected_error_code, expected_error_message); + + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), expected_error_code); + ASSERT_EQ(details.GetErrorMessage(), expected_error_message); + ASSERT_NO_THROW(details.GetFlagMetadata()); +} + +TEST(ResolutionDetailsTest, AccessesFieldsAfterInitializationForObject) { + const Value expected_value = Value(std::map{ + {"key1", Value("value1")}, {"key2", Value(42)}}); + const Reason expected_reason = Reason::kTargetingMatch; + const std::optional expected_variant = "on-variant"; + const std::optional expected_error_code = ErrorCode::kParseError; + const std::optional expected_error_message = + "Failed to parse data"; + + const FlagMetadata expected_flag_metadata{}; + + ObjectResolutionDetails details(expected_value, expected_reason, + expected_variant, expected_flag_metadata, + expected_error_code, expected_error_message); + + EXPECT_EQ(details.GetValue(), expected_value); + ASSERT_EQ(details.GetReason(), expected_reason); + ASSERT_EQ(details.GetVariant(), expected_variant); + ASSERT_EQ(details.GetErrorCode(), expected_error_code); + ASSERT_EQ(details.GetErrorMessage(), expected_error_message); + ASSERT_NO_THROW(details.GetFlagMetadata()); } From 09c25304a453c8fa017a6287703d133908fedf9d Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Fri, 13 Feb 2026 14:49:30 +0000 Subject: [PATCH 2/4] Fix comments and add unit tests Signed-off-by: NeaguGeorgiana23 --- .gitmodules | 3 + openfeature/value.cpp | 29 +++- test/BUILD | 9 + test/value_test.cpp | 378 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 .gitmodules create mode 100644 test/value_test.cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..85115b5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "spec"] + path = spec + url = https://github.com/open-feature/spec.git diff --git a/openfeature/value.cpp b/openfeature/value.cpp index 9b3b296..84edd01 100644 --- a/openfeature/value.cpp +++ b/openfeature/value.cpp @@ -15,7 +15,11 @@ Value::Value(double value) : inner_value_(value) {} Value::Value(std::string value) : inner_value_(std::move(value)) {} -Value::Value(const char* value) : inner_value_(std::string(value)) {} +Value::Value(const char* value) : inner_value_() { + if (value) { + inner_value_ = std::string(value); + } +} Value::Value(const std::map& value) : inner_value_(std::make_unique>(value)) {} @@ -30,12 +34,13 @@ Value::Value(const Value& other) { if (other.IsStructure()) { auto& ptr = std::get>>( other.inner_value_); - inner_value_ = - ptr ? std::make_unique>(*ptr) : nullptr; + inner_value_ = ptr ? std::make_unique>(*ptr) + : std::unique_ptr>(nullptr); } else if (other.IsList()) { auto& ptr = std::get>>(other.inner_value_); - inner_value_ = ptr ? std::make_unique>(*ptr) : nullptr; + inner_value_ = ptr ? std::make_unique>(*ptr) + : std::unique_ptr>(nullptr); } else { std::visit( [this](auto&& arg) { @@ -97,7 +102,7 @@ std::optional Value::AsString() const { std::optional Value::AsInt() const { if (auto* v = std::get_if(&inner_value_)) return *v; if (auto* v = std::get_if(&inner_value_)) - return static_cast(std::round(*v)); + return static_cast(std::floor(*v + 0.5)); return std::nullopt; } @@ -144,10 +149,20 @@ bool operator==(const Value& lhs, const Value& rhs) { return lhs.AsDateTime() == rhs.AsDateTime(); if (lhs.IsStructure() && rhs.IsStructure()) { - return *lhs.AsStructure() == *rhs.AsStructure(); + const auto* lhs_struct = lhs.AsStructure(); + const auto* rhs_struct = rhs.AsStructure(); + if (lhs_struct && rhs_struct) { + return *lhs_struct == *rhs_struct; + } + return lhs_struct == rhs_struct; } if (lhs.IsList() && rhs.IsList()) { - return *lhs.AsList() == *rhs.AsList(); + const auto* lhs_list = lhs.AsList(); + const auto* rhs_list = rhs.AsList(); + if (lhs_list && rhs_list) { + return *lhs_list == *rhs_list; + } + return lhs_list == rhs_list; } return false; diff --git a/test/BUILD b/test/BUILD index 97e762c..72c9ef8 100644 --- a/test/BUILD +++ b/test/BUILD @@ -86,3 +86,12 @@ cc_test( "@googletest//:gtest_main", ], ) + +cc_test( + name = "value_test", + srcs = ["value_test.cpp"], + deps = [ + "//openfeature:value", + "@googletest//:gtest_main", + ], +) diff --git a/test/value_test.cpp b/test/value_test.cpp new file mode 100644 index 0000000..014aff2 --- /dev/null +++ b/test/value_test.cpp @@ -0,0 +1,378 @@ +#include "openfeature/value.h" + +#include +#include +#include +#include + +#include "gtest/gtest.h" + +namespace openfeature { + +using namespace std::chrono_literals; + +TEST(ValueTest, DefaultConstructorIsNull) { + Value value; + EXPECT_TRUE(value.IsNull()); + EXPECT_FALSE(value.IsBool()); + EXPECT_FALSE(value.IsNumber()); + EXPECT_FALSE(value.IsString()); + EXPECT_FALSE(value.IsStructure()); + EXPECT_FALSE(value.IsList()); + EXPECT_FALSE(value.IsDateTime()); +} + +TEST(ValueTest, BoolConstructorAndAccessors) { + Value true_val(true); + EXPECT_FALSE(true_val.IsNull()); + EXPECT_TRUE(true_val.IsBool()); + EXPECT_EQ(true_val.AsBool(), true); + EXPECT_FALSE(true_val.AsInt().has_value()); + EXPECT_FALSE(true_val.AsString().has_value()); + + Value false_val(false); + EXPECT_EQ(false_val.AsBool(), false); +} + +TEST(ValueTest, Int64ConstructorAndAccessors) { + Value int_val(static_cast(123LL)); + EXPECT_FALSE(int_val.IsNull()); + EXPECT_TRUE(int_val.IsNumber()); + EXPECT_EQ(int_val.AsInt(), 123LL); + EXPECT_EQ(int_val.AsDouble(), 123.0); + EXPECT_FALSE(int_val.AsBool().has_value()); + + Value int_min(std::numeric_limits::min()); + EXPECT_EQ(int_min.AsInt(), std::numeric_limits::min()); + + Value int_max(std::numeric_limits::max()); + EXPECT_EQ(int_max.AsInt(), std::numeric_limits::max()); +} + +TEST(ValueTest, IntConstructorAndAccessors) { + Value int_val(456); + EXPECT_FALSE(int_val.IsNull()); + EXPECT_TRUE(int_val.IsNumber()); + EXPECT_EQ(int_val.AsInt(), 456LL); + EXPECT_EQ(int_val.AsDouble(), 456.0); +} + +TEST(ValueTest, DoubleConstructorAndAccessors) { + Value double_val(123.45); + EXPECT_FALSE(double_val.IsNull()); + EXPECT_TRUE(double_val.IsNumber()); + EXPECT_EQ(double_val.AsDouble(), 123.45); + EXPECT_EQ(double_val.AsInt(), 123LL); // Should round + EXPECT_FALSE(double_val.AsBool().has_value()); + + Value double_round_up(123.5); + EXPECT_EQ(double_round_up.AsInt(), 124LL); + + Value double_round_down(123.4); + EXPECT_EQ(double_round_down.AsInt(), 123LL); +} + +TEST(ValueTest, StringConstructorAndAccessors) { + Value string_val(std::string("hello")); + EXPECT_FALSE(string_val.IsNull()); + EXPECT_TRUE(string_val.IsString()); + EXPECT_EQ(string_val.AsString(), "hello"); + EXPECT_FALSE(string_val.AsBool().has_value()); + + Value c_string_val("world"); + EXPECT_TRUE(c_string_val.IsString()); + EXPECT_EQ(c_string_val.AsString(), "world"); +} + +TEST(ValueTest, MapConstructorAndAccessors) { + std::map test_map; + test_map["key1"] = Value("value1"); + test_map["key2"] = Value(true); + test_map["key3"] = Value(123); + + Value map_val(test_map); + EXPECT_FALSE(map_val.IsNull()); + EXPECT_TRUE(map_val.IsStructure()); + EXPECT_NE(map_val.AsStructure(), nullptr); + ASSERT_TRUE(map_val.AsStructure()->count("key1")); + EXPECT_EQ(map_val.AsStructure()->at("key1").AsString(), "value1"); + EXPECT_EQ(map_val.AsStructure()->at("key2").AsBool(), true); + EXPECT_EQ(map_val.AsStructure()->at("key3").AsInt(), 123LL); + + EXPECT_FALSE(map_val.AsBool().has_value()); + EXPECT_EQ(map_val.AsList(), nullptr); + + Value empty_map_val(std::map{}); + EXPECT_TRUE(empty_map_val.IsStructure()); + EXPECT_NE(empty_map_val.AsStructure(), nullptr); + EXPECT_TRUE(empty_map_val.AsStructure()->empty()); +} + +TEST(ValueTest, VectorConstructorAndAccessors) { + std::vector test_list; + test_list.push_back(Value("item1")); + test_list.push_back(Value(false)); + test_list.push_back(Value(4.5)); + + Value list_val(test_list); + EXPECT_FALSE(list_val.IsNull()); + EXPECT_TRUE(list_val.IsList()); + EXPECT_NE(list_val.AsList(), nullptr); + ASSERT_EQ(list_val.AsList()->size(), 3); + EXPECT_EQ(list_val.AsList()->at(0).AsString(), "item1"); + EXPECT_EQ(list_val.AsList()->at(1).AsBool(), false); + EXPECT_EQ(list_val.AsList()->at(2).AsDouble(), 4.5); + + EXPECT_FALSE(list_val.AsInt().has_value()); + EXPECT_EQ(list_val.AsStructure(), nullptr); + + Value empty_list_val(std::vector{}); + EXPECT_TRUE(empty_list_val.IsList()); + EXPECT_NE(empty_list_val.AsList(), nullptr); + EXPECT_TRUE(empty_list_val.AsList()->empty()); +} + +TEST(ValueTest, DateTimeConstructorAndAccessors) { + auto now = std::chrono::system_clock::now(); + Value dt_val(now); + EXPECT_FALSE(dt_val.IsNull()); + EXPECT_TRUE(dt_val.IsDateTime()); + EXPECT_EQ(dt_val.AsDateTime(), now); + EXPECT_FALSE(dt_val.AsBool().has_value()); +} + +TEST(ValueTest, CopyConstructor) { + std::map inner_map; + inner_map["nested_key"] = Value(100); + std::vector inner_list = {Value("list_item")}; + + std::map original_map; + original_map["bool"] = Value(true); + original_map["int"] = Value(123); + original_map["str"] = Value("original"); + original_map["sub_map"] = Value(inner_map); + original_map["sub_list"] = Value(inner_list); + + Value original_value(original_map); + + Value copied_value = original_value; + + EXPECT_EQ(copied_value, original_value); + + EXPECT_NE(original_value.AsStructure(), nullptr); + EXPECT_NE(copied_value.AsStructure(), nullptr); + + original_map["bool"] = Value(false); + original_value = Value(original_map); + + EXPECT_NE(copied_value, original_value); + EXPECT_EQ(copied_value.AsStructure()->at("bool").AsBool(), true); + EXPECT_EQ(original_value.AsStructure()->at("bool").AsBool(), false); + + ASSERT_NE(original_value.AsStructure()->at("sub_map").AsStructure(), nullptr); + ASSERT_NE(copied_value.AsStructure()->at("sub_map").AsStructure(), nullptr); + EXPECT_EQ(original_value.AsStructure() + ->at("sub_map") + .AsStructure() + ->at("nested_key") + .AsInt(), + 100LL); + EXPECT_EQ(copied_value.AsStructure() + ->at("sub_map") + .AsStructure() + ->at("nested_key") + .AsInt(), + 100LL); + + std::map original_nested_map = + *original_value.AsStructure()->at("sub_map").AsStructure(); + original_nested_map["nested_key"] = Value(200); + original_map["sub_map"] = Value(original_nested_map); + original_value = Value(original_map); + + EXPECT_EQ(original_value.AsStructure() + ->at("sub_map") + .AsStructure() + ->at("nested_key") + .AsInt(), + 200LL); + EXPECT_EQ(copied_value.AsStructure() + ->at("sub_map") + .AsStructure() + ->at("nested_key") + .AsInt(), + 100LL); +} + +TEST(ValueTest, AssignmentOperator) { + Value val1(10); + Value val2("test"); + Value val3; + + val3 = val1; + EXPECT_EQ(val3.AsInt(), 10LL); + EXPECT_EQ(val3, val1); + + val3 = val2; + EXPECT_EQ(val3.AsString(), "test"); + EXPECT_EQ(val3, val2); + + val3 = val3; + EXPECT_EQ(val3.AsString(), "test"); + + std::map original_map; + original_map["key"] = Value(5); + Value map_val(original_map); + + Value assigned_map_val; + assigned_map_val = map_val; + EXPECT_EQ(assigned_map_val, map_val); + + std::map modified_map = *map_val.AsStructure(); + modified_map["key"] = Value(6); + map_val = Value(modified_map); + + EXPECT_NE(assigned_map_val, map_val); + EXPECT_EQ(assigned_map_val.AsStructure()->at("key").AsInt(), 5LL); + EXPECT_EQ(map_val.AsStructure()->at("key").AsInt(), 6LL); +} + +TEST(ValueTest, IsNumberHandlesIntAndDouble) { + Value int_val(100); + EXPECT_TRUE(int_val.IsNumber()); + + Value double_val(3.14); + EXPECT_TRUE(double_val.IsNumber()); + + Value bool_val(true); + EXPECT_FALSE(bool_val.IsNumber()); + + Value string_val("hello"); + EXPECT_FALSE(string_val.IsNumber()); +} + +TEST(ValueTest, AsNumberConversions) { + Value int_val(5); + EXPECT_EQ(int_val.AsInt(), 5LL); + EXPECT_EQ(int_val.AsDouble(), 5.0); + + Value double_val(5.7); + EXPECT_EQ(double_val.AsInt(), 6LL); + EXPECT_EQ(double_val.AsDouble(), 5.7); + + Value double_val_negative(-5.3); + EXPECT_EQ(double_val_negative.AsInt(), + -5LL); // Rounds to nearest even on .5, otherwise standard rounding + EXPECT_EQ(double_val_negative.AsDouble(), -5.3); + + Value double_val_negative_half(-5.5); + EXPECT_EQ(double_val_negative_half.AsInt(), -5LL); // Rounds to nearest even +} + +TEST(ValueTest, EqualityOperator_BasicTypes) { + EXPECT_TRUE(Value(true) == Value(true)); + EXPECT_FALSE(Value(true) == Value(false)); + EXPECT_TRUE(Value(10) == Value(10)); + EXPECT_FALSE(Value(10) == Value(11)); + EXPECT_TRUE(Value(3.14) == Value(3.14)); + EXPECT_FALSE(Value(3.14) == Value(3.15)); + EXPECT_TRUE(Value("test") == Value("test")); + EXPECT_FALSE(Value("test") == Value("other")); + EXPECT_TRUE(Value() == Value()); + + auto t1 = std::chrono::system_clock::now(); + auto t2 = t1 + 1s; + EXPECT_TRUE(Value(t1) == Value(t1)); + EXPECT_FALSE(Value(t1) == Value(t2)); +} + +TEST(ValueTest, EqualityOperator_NumberCrossTypes) { + EXPECT_TRUE(Value(5) == Value(5.0)); + EXPECT_TRUE(Value(5.0) == Value(5)); + EXPECT_FALSE(Value(5) == Value(5.1)); + EXPECT_FALSE(Value(5.1) == Value(5)); + EXPECT_TRUE(Value(std::numeric_limits::max()) == + Value(static_cast(std::numeric_limits::max()))); +} + +TEST(ValueTest, EqualityOperator_DifferentTypes) { + EXPECT_FALSE(Value(true) == Value(1)); + EXPECT_FALSE(Value(10) == Value("10")); + EXPECT_FALSE(Value(3.14) == Value(true)); + EXPECT_FALSE(Value("hello") == Value()); + EXPECT_FALSE(Value() == Value(false)); +} + +TEST(ValueTest, EqualityOperator_ComplexTypes) { + std::map map1 = {{"a", Value(1)}, {"b", Value("x")}}; + std::map map2 = {{"a", Value(1)}, {"b", Value("x")}}; + std::map map3 = {{"a", Value(1)}, {"b", Value("y")}}; + std::map map4 = {{"a", Value(1)}}; + + EXPECT_TRUE(Value(map1) == Value(map2)); + EXPECT_FALSE(Value(map1) == Value(map3)); + EXPECT_FALSE(Value(map1) == Value(map4)); + EXPECT_FALSE(Value(map1) == Value()); + + std::vector list1 = {Value(1), Value("x")}; + std::vector list2 = {Value(1), Value("x")}; + std::vector list3 = {Value(1), Value("y")}; + std::vector list4 = {Value(1)}; + + EXPECT_TRUE(Value(list1) == Value(list2)); + EXPECT_FALSE(Value(list1) == Value(list3)); + EXPECT_FALSE(Value(list1) == Value(list4)); + EXPECT_FALSE(Value(list1) == Value()); + + std::map nested_map1 = {{"key", Value(list1)}}; + std::map nested_map2 = {{"key", Value(list2)}}; + std::map nested_map3 = {{"key", Value(list3)}}; + + EXPECT_TRUE(Value(nested_map1) == Value(nested_map2)); + EXPECT_FALSE(Value(nested_map1) == Value(nested_map3)); +} + +TEST(ValueTest, InequalityOperator) { + EXPECT_TRUE(Value(true) != Value(false)); + EXPECT_TRUE(Value(10) != Value(11)); + EXPECT_TRUE(Value("test") != Value("other")); + EXPECT_TRUE(Value(5) != Value(5.1)); + EXPECT_TRUE(Value(true) != Value(1)); + EXPECT_TRUE(Value("hello") != Value()); + + std::map map1 = {{"a", Value(1)}}; + std::map map2 = {{"a", Value(2)}}; + EXPECT_TRUE(Value(map1) != Value(map2)); + + std::vector list1 = {Value(1)}; + std::vector list2 = {Value(2)}; + EXPECT_TRUE(Value(list1) != Value(list2)); + + EXPECT_FALSE(Value(true) != Value(true)); + EXPECT_FALSE(Value() != Value()); + EXPECT_FALSE(Value(5) != Value(5.0)); +} + +TEST(ValueTest, MoveConstructorAndAssignmentDefaulted) { + // Move constructor + Value original_string("move me"); + Value moved_string = std::move(original_string); + EXPECT_TRUE(moved_string.IsString()); + EXPECT_EQ(moved_string.AsString(), "move me"); + EXPECT_EQ(original_string.AsString(), ""); + + // Move assignment + Value original_map_val; + std::map data_map = {{"key", Value(100)}}; + original_map_val = Value(data_map); + + Value target_val("old value"); + target_val = std::move(original_map_val); + + EXPECT_TRUE(target_val.IsStructure()); + EXPECT_NE(target_val.AsStructure(), nullptr); + EXPECT_EQ(target_val.AsStructure()->at("key").AsInt(), 100LL); + EXPECT_EQ(original_map_val.AsStructure(), nullptr); +} + +} // namespace openfeature \ No newline at end of file From ffdf72fd347358816af9b2beb26c3a6963a0fabc Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Fri, 13 Feb 2026 14:49:30 +0000 Subject: [PATCH 3/4] Fix comments and add unit tests Signed-off-by: NeaguGeorgiana23 --- .gitmodules | 3 + openfeature/noop_provider.h | 9 +- openfeature/value.cpp | 29 ++- test/BUILD | 9 + test/value_test.cpp | 373 ++++++++++++++++++++++++++++++++++++ 5 files changed, 412 insertions(+), 11 deletions(-) create mode 100644 .gitmodules create mode 100644 test/value_test.cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..85115b5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "spec"] + path = spec + url = https://github.com/open-feature/spec.git diff --git a/openfeature/noop_provider.h b/openfeature/noop_provider.h index d15e408..7ccd6f8 100644 --- a/openfeature/noop_provider.h +++ b/openfeature/noop_provider.h @@ -30,21 +30,22 @@ class NoopProvider : public FeatureProvider { // StringResolutionDetails returns a string flag. std::unique_ptr GetStringEvaluation( std::string_view flag, std::string_view default_value, - const EvaluationContext& ctx); + const EvaluationContext& ctx) override; // IntResolutionDetails returns an integer flag. std::unique_ptr GetIntegerEvaluation( std::string_view flag, int64_t default_value, - const EvaluationContext& ctx); + const EvaluationContext& ctx) override; // DoubleResolutionDetails returns a double flag. std::unique_ptr GetDoubleEvaluation( std::string_view flag, double default_value, - const EvaluationContext& ctx); + const EvaluationContext& ctx) override; // ObjectResolutionDetails returns an object flag. std::unique_ptr GetObjectEvaluation( - std::string_view flag, Value default_value, const EvaluationContext& ctx); + std::string_view flag, Value default_value, + const EvaluationContext& ctx) override; private: std::string name_ = "Noop Provider"; diff --git a/openfeature/value.cpp b/openfeature/value.cpp index 9b3b296..84edd01 100644 --- a/openfeature/value.cpp +++ b/openfeature/value.cpp @@ -15,7 +15,11 @@ Value::Value(double value) : inner_value_(value) {} Value::Value(std::string value) : inner_value_(std::move(value)) {} -Value::Value(const char* value) : inner_value_(std::string(value)) {} +Value::Value(const char* value) : inner_value_() { + if (value) { + inner_value_ = std::string(value); + } +} Value::Value(const std::map& value) : inner_value_(std::make_unique>(value)) {} @@ -30,12 +34,13 @@ Value::Value(const Value& other) { if (other.IsStructure()) { auto& ptr = std::get>>( other.inner_value_); - inner_value_ = - ptr ? std::make_unique>(*ptr) : nullptr; + inner_value_ = ptr ? std::make_unique>(*ptr) + : std::unique_ptr>(nullptr); } else if (other.IsList()) { auto& ptr = std::get>>(other.inner_value_); - inner_value_ = ptr ? std::make_unique>(*ptr) : nullptr; + inner_value_ = ptr ? std::make_unique>(*ptr) + : std::unique_ptr>(nullptr); } else { std::visit( [this](auto&& arg) { @@ -97,7 +102,7 @@ std::optional Value::AsString() const { std::optional Value::AsInt() const { if (auto* v = std::get_if(&inner_value_)) return *v; if (auto* v = std::get_if(&inner_value_)) - return static_cast(std::round(*v)); + return static_cast(std::floor(*v + 0.5)); return std::nullopt; } @@ -144,10 +149,20 @@ bool operator==(const Value& lhs, const Value& rhs) { return lhs.AsDateTime() == rhs.AsDateTime(); if (lhs.IsStructure() && rhs.IsStructure()) { - return *lhs.AsStructure() == *rhs.AsStructure(); + const auto* lhs_struct = lhs.AsStructure(); + const auto* rhs_struct = rhs.AsStructure(); + if (lhs_struct && rhs_struct) { + return *lhs_struct == *rhs_struct; + } + return lhs_struct == rhs_struct; } if (lhs.IsList() && rhs.IsList()) { - return *lhs.AsList() == *rhs.AsList(); + const auto* lhs_list = lhs.AsList(); + const auto* rhs_list = rhs.AsList(); + if (lhs_list && rhs_list) { + return *lhs_list == *rhs_list; + } + return lhs_list == rhs_list; } return false; diff --git a/test/BUILD b/test/BUILD index 97e762c..72c9ef8 100644 --- a/test/BUILD +++ b/test/BUILD @@ -86,3 +86,12 @@ cc_test( "@googletest//:gtest_main", ], ) + +cc_test( + name = "value_test", + srcs = ["value_test.cpp"], + deps = [ + "//openfeature:value", + "@googletest//:gtest_main", + ], +) diff --git a/test/value_test.cpp b/test/value_test.cpp new file mode 100644 index 0000000..8c0be17 --- /dev/null +++ b/test/value_test.cpp @@ -0,0 +1,373 @@ +#include "openfeature/value.h" + +#include +#include +#include +#include + +#include "gtest/gtest.h" + +namespace openfeature { + +using namespace std::chrono_literals; + +TEST(ValueTest, DefaultConstructorIsNull) { + Value value; + EXPECT_TRUE(value.IsNull()); + EXPECT_FALSE(value.IsBool()); + EXPECT_FALSE(value.IsNumber()); + EXPECT_FALSE(value.IsString()); + EXPECT_FALSE(value.IsStructure()); + EXPECT_FALSE(value.IsList()); + EXPECT_FALSE(value.IsDateTime()); +} + +TEST(ValueTest, BoolConstructorAndAccessors) { + Value true_val(true); + EXPECT_FALSE(true_val.IsNull()); + EXPECT_TRUE(true_val.IsBool()); + EXPECT_EQ(true_val.AsBool(), true); + EXPECT_FALSE(true_val.AsInt().has_value()); + EXPECT_FALSE(true_val.AsString().has_value()); + + Value false_val(false); + EXPECT_EQ(false_val.AsBool(), false); +} + +TEST(ValueTest, Int64ConstructorAndAccessors) { + Value int_val(static_cast(123LL)); + EXPECT_FALSE(int_val.IsNull()); + EXPECT_TRUE(int_val.IsNumber()); + EXPECT_EQ(int_val.AsInt(), 123LL); + EXPECT_EQ(int_val.AsDouble(), 123.0); + EXPECT_FALSE(int_val.AsBool().has_value()); + + Value int_min(std::numeric_limits::min()); + EXPECT_EQ(int_min.AsInt(), std::numeric_limits::min()); + + Value int_max(std::numeric_limits::max()); + EXPECT_EQ(int_max.AsInt(), std::numeric_limits::max()); +} + +TEST(ValueTest, IntConstructorAndAccessors) { + Value int_val(456); + EXPECT_FALSE(int_val.IsNull()); + EXPECT_TRUE(int_val.IsNumber()); + EXPECT_EQ(int_val.AsInt(), 456LL); + EXPECT_EQ(int_val.AsDouble(), 456.0); +} + +TEST(ValueTest, DoubleConstructorAndAccessors) { + Value double_val(123.45); + EXPECT_FALSE(double_val.IsNull()); + EXPECT_TRUE(double_val.IsNumber()); + EXPECT_EQ(double_val.AsDouble(), 123.45); + EXPECT_EQ(double_val.AsInt(), 123LL); // Should round + EXPECT_FALSE(double_val.AsBool().has_value()); + + Value double_round_up(123.5); + EXPECT_EQ(double_round_up.AsInt(), 124LL); + + Value double_round_down(123.4); + EXPECT_EQ(double_round_down.AsInt(), 123LL); +} + +TEST(ValueTest, StringConstructorAndAccessors) { + Value string_val(std::string("hello")); + EXPECT_FALSE(string_val.IsNull()); + EXPECT_TRUE(string_val.IsString()); + EXPECT_EQ(string_val.AsString(), "hello"); + EXPECT_FALSE(string_val.AsBool().has_value()); + + Value c_string_val("world"); + EXPECT_TRUE(c_string_val.IsString()); + EXPECT_EQ(c_string_val.AsString(), "world"); +} + +TEST(ValueTest, MapConstructorAndAccessors) { + std::map test_map; + test_map["key1"] = Value("value1"); + test_map["key2"] = Value(true); + test_map["key3"] = Value(123); + + Value map_val(test_map); + EXPECT_FALSE(map_val.IsNull()); + EXPECT_TRUE(map_val.IsStructure()); + EXPECT_NE(map_val.AsStructure(), nullptr); + ASSERT_TRUE(map_val.AsStructure()->count("key1")); + EXPECT_EQ(map_val.AsStructure()->at("key1").AsString(), "value1"); + EXPECT_EQ(map_val.AsStructure()->at("key2").AsBool(), true); + EXPECT_EQ(map_val.AsStructure()->at("key3").AsInt(), 123LL); + + EXPECT_FALSE(map_val.AsBool().has_value()); + EXPECT_EQ(map_val.AsList(), nullptr); + + Value empty_map_val(std::map{}); + EXPECT_TRUE(empty_map_val.IsStructure()); + EXPECT_NE(empty_map_val.AsStructure(), nullptr); + EXPECT_TRUE(empty_map_val.AsStructure()->empty()); +} + +TEST(ValueTest, VectorConstructorAndAccessors) { + std::vector test_list; + test_list.push_back(Value("item1")); + test_list.push_back(Value(false)); + test_list.push_back(Value(4.5)); + + Value list_val(test_list); + EXPECT_FALSE(list_val.IsNull()); + EXPECT_TRUE(list_val.IsList()); + EXPECT_NE(list_val.AsList(), nullptr); + ASSERT_EQ(list_val.AsList()->size(), 3); + EXPECT_EQ(list_val.AsList()->at(0).AsString(), "item1"); + EXPECT_EQ(list_val.AsList()->at(1).AsBool(), false); + EXPECT_EQ(list_val.AsList()->at(2).AsDouble(), 4.5); + + EXPECT_FALSE(list_val.AsInt().has_value()); + EXPECT_EQ(list_val.AsStructure(), nullptr); + + Value empty_list_val(std::vector{}); + EXPECT_TRUE(empty_list_val.IsList()); + EXPECT_NE(empty_list_val.AsList(), nullptr); + EXPECT_TRUE(empty_list_val.AsList()->empty()); +} + +TEST(ValueTest, DateTimeConstructorAndAccessors) { + auto now = std::chrono::system_clock::now(); + Value dt_val(now); + EXPECT_FALSE(dt_val.IsNull()); + EXPECT_TRUE(dt_val.IsDateTime()); + EXPECT_EQ(dt_val.AsDateTime(), now); + EXPECT_FALSE(dt_val.AsBool().has_value()); +} + +TEST(ValueTest, CopyConstructor) { + std::map inner_map; + inner_map["nested_key"] = Value(100); + std::vector inner_list = {Value("list_item")}; + + std::map original_map; + original_map["bool"] = Value(true); + original_map["int"] = Value(123); + original_map["str"] = Value("original"); + original_map["sub_map"] = Value(inner_map); + original_map["sub_list"] = Value(inner_list); + + Value original_value(original_map); + + Value copied_value = original_value; + + EXPECT_EQ(copied_value, original_value); + + EXPECT_NE(original_value.AsStructure(), nullptr); + EXPECT_NE(copied_value.AsStructure(), nullptr); + + original_map["bool"] = Value(false); + original_value = Value(original_map); + + EXPECT_NE(copied_value, original_value); + EXPECT_EQ(copied_value.AsStructure()->at("bool").AsBool(), true); + EXPECT_EQ(original_value.AsStructure()->at("bool").AsBool(), false); + + ASSERT_NE(original_value.AsStructure()->at("sub_map").AsStructure(), nullptr); + ASSERT_NE(copied_value.AsStructure()->at("sub_map").AsStructure(), nullptr); + EXPECT_EQ(original_value.AsStructure() + ->at("sub_map") + .AsStructure() + ->at("nested_key") + .AsInt(), + 100LL); + EXPECT_EQ(copied_value.AsStructure() + ->at("sub_map") + .AsStructure() + ->at("nested_key") + .AsInt(), + 100LL); + + std::map original_nested_map = + *original_value.AsStructure()->at("sub_map").AsStructure(); + original_nested_map["nested_key"] = Value(200); + original_map["sub_map"] = Value(original_nested_map); + original_value = Value(original_map); + + EXPECT_EQ(original_value.AsStructure() + ->at("sub_map") + .AsStructure() + ->at("nested_key") + .AsInt(), + 200LL); + EXPECT_EQ(copied_value.AsStructure() + ->at("sub_map") + .AsStructure() + ->at("nested_key") + .AsInt(), + 100LL); +} + +TEST(ValueTest, AssignmentOperator) { + Value val1(10); + Value val2("test"); + Value val3; + + val3 = val1; + EXPECT_EQ(val3.AsInt(), 10LL); + EXPECT_EQ(val3, val1); + + val3 = val2; + EXPECT_EQ(val3.AsString(), "test"); + EXPECT_EQ(val3, val2); + + std::map original_map; + original_map["key"] = Value(5); + Value map_val(original_map); + + Value assigned_map_val; + assigned_map_val = map_val; + EXPECT_EQ(assigned_map_val, map_val); + + std::map modified_map = *map_val.AsStructure(); + modified_map["key"] = Value(6); + map_val = Value(modified_map); + + EXPECT_NE(assigned_map_val, map_val); + EXPECT_EQ(assigned_map_val.AsStructure()->at("key").AsInt(), 5LL); + EXPECT_EQ(map_val.AsStructure()->at("key").AsInt(), 6LL); +} + +TEST(ValueTest, IsNumberHandlesIntAndDouble) { + Value int_val(100); + EXPECT_TRUE(int_val.IsNumber()); + + Value double_val(3.14); + EXPECT_TRUE(double_val.IsNumber()); + + Value bool_val(true); + EXPECT_FALSE(bool_val.IsNumber()); + + Value string_val("hello"); + EXPECT_FALSE(string_val.IsNumber()); +} + +TEST(ValueTest, AsNumberConversions) { + Value int_val(5); + EXPECT_EQ(int_val.AsInt(), 5LL); + EXPECT_EQ(int_val.AsDouble(), 5.0); + + Value double_val(5.7); + EXPECT_EQ(double_val.AsInt(), 6LL); + EXPECT_EQ(double_val.AsDouble(), 5.7); + + Value double_val_negative(-5.3); + EXPECT_EQ(double_val_negative.AsInt(), + -5LL); // Rounds to nearest even on .5, otherwise standard rounding + EXPECT_EQ(double_val_negative.AsDouble(), -5.3); + + Value double_val_negative_half(-5.5); + EXPECT_EQ(double_val_negative_half.AsInt(), -5LL); // Rounds to nearest even +} + +TEST(ValueTest, EqualityOperator_BasicTypes) { + EXPECT_TRUE(Value(true) == Value(true)); + EXPECT_FALSE(Value(true) == Value(false)); + EXPECT_TRUE(Value(10) == Value(10)); + EXPECT_FALSE(Value(10) == Value(11)); + EXPECT_TRUE(Value(3.14) == Value(3.14)); + EXPECT_FALSE(Value(3.14) == Value(3.15)); + EXPECT_TRUE(Value("test") == Value("test")); + EXPECT_FALSE(Value("test") == Value("other")); + EXPECT_TRUE(Value() == Value()); + + auto t1 = std::chrono::system_clock::now(); + auto t2 = t1 + 1s; + EXPECT_TRUE(Value(t1) == Value(t1)); + EXPECT_FALSE(Value(t1) == Value(t2)); +} + +TEST(ValueTest, EqualityOperator_NumberCrossTypes) { + EXPECT_TRUE(Value(5) == Value(5.0)); + EXPECT_TRUE(Value(5.0) == Value(5)); + EXPECT_FALSE(Value(5) == Value(5.1)); + EXPECT_FALSE(Value(5.1) == Value(5)); + EXPECT_TRUE(Value(std::numeric_limits::max()) == + Value(static_cast(std::numeric_limits::max()))); +} + +TEST(ValueTest, EqualityOperator_DifferentTypes) { + EXPECT_FALSE(Value(true) == Value(1)); + EXPECT_FALSE(Value(10) == Value("10")); + EXPECT_FALSE(Value(3.14) == Value(true)); + EXPECT_FALSE(Value("hello") == Value()); + EXPECT_FALSE(Value() == Value(false)); +} + +TEST(ValueTest, EqualityOperator_ComplexTypes) { + std::map map1 = {{"a", Value(1)}, {"b", Value("x")}}; + std::map map2 = {{"a", Value(1)}, {"b", Value("x")}}; + std::map map3 = {{"a", Value(1)}, {"b", Value("y")}}; + std::map map4 = {{"a", Value(1)}}; + + EXPECT_TRUE(Value(map1) == Value(map2)); + EXPECT_FALSE(Value(map1) == Value(map3)); + EXPECT_FALSE(Value(map1) == Value(map4)); + EXPECT_FALSE(Value(map1) == Value()); + + std::vector list1 = {Value(1), Value("x")}; + std::vector list2 = {Value(1), Value("x")}; + std::vector list3 = {Value(1), Value("y")}; + std::vector list4 = {Value(1)}; + + EXPECT_TRUE(Value(list1) == Value(list2)); + EXPECT_FALSE(Value(list1) == Value(list3)); + EXPECT_FALSE(Value(list1) == Value(list4)); + EXPECT_FALSE(Value(list1) == Value()); + + std::map nested_map1 = {{"key", Value(list1)}}; + std::map nested_map2 = {{"key", Value(list2)}}; + std::map nested_map3 = {{"key", Value(list3)}}; + + EXPECT_TRUE(Value(nested_map1) == Value(nested_map2)); + EXPECT_FALSE(Value(nested_map1) == Value(nested_map3)); +} + +TEST(ValueTest, InequalityOperator) { + EXPECT_TRUE(Value(true) != Value(false)); + EXPECT_TRUE(Value(10) != Value(11)); + EXPECT_TRUE(Value("test") != Value("other")); + EXPECT_TRUE(Value(5) != Value(5.1)); + EXPECT_TRUE(Value(true) != Value(1)); + EXPECT_TRUE(Value("hello") != Value()); + + std::map map1 = {{"a", Value(1)}}; + std::map map2 = {{"a", Value(2)}}; + EXPECT_TRUE(Value(map1) != Value(map2)); + + std::vector list1 = {Value(1)}; + std::vector list2 = {Value(2)}; + EXPECT_TRUE(Value(list1) != Value(list2)); + + EXPECT_FALSE(Value(true) != Value(true)); + EXPECT_FALSE(Value() != Value()); + EXPECT_FALSE(Value(5) != Value(5.0)); +} + +TEST(ValueTest, MoveConstructorAndAssignmentDefaulted) { + // Move constructor + Value original_string("move me"); + Value moved_string = std::move(original_string); + EXPECT_TRUE(moved_string.IsString()); + EXPECT_EQ(moved_string.AsString(), "move me"); + + // Move assignment + Value original_map_val; + std::map data_map = {{"key", Value(100)}}; + original_map_val = Value(data_map); + + Value target_val("old value"); + target_val = std::move(original_map_val); + + EXPECT_TRUE(target_val.IsStructure()); + EXPECT_NE(target_val.AsStructure(), nullptr); + EXPECT_EQ(target_val.AsStructure()->at("key").AsInt(), 100LL); +} + +} // namespace openfeature \ No newline at end of file From c527d5a73c2830ceae34775b79233e1512c9c0e5 Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Mon, 16 Feb 2026 14:24:18 +0000 Subject: [PATCH 4/4] Update Mrege Logic Signed-off-by: NeaguGeorgiana23 --- openfeature/client_api.cpp | 14 ++++-- test/client_api_test.cpp | 96 +++++++++++++++++++++++++++++++------- 2 files changed, 89 insertions(+), 21 deletions(-) diff --git a/openfeature/client_api.cpp b/openfeature/client_api.cpp index af404b6..58680a4 100644 --- a/openfeature/client_api.cpp +++ b/openfeature/client_api.cpp @@ -144,12 +144,16 @@ std::unique_ptr ClientAPI::EvaluateObjectFlag( EvaluationContext ClientAPI::MergeContexts( const std::optional& invocation_ctx) { - // TODO: Add context merging logic after EvaluationContext is implemented. - - if (invocation_ctx) { - return *invocation_ctx; + EvaluationContext global_ctx = + GlobalContextManager::GetInstance().GetGlobalEvaluationContext(); + EvaluationContext client_ctx = GetEvaluationContext(); + + if (invocation_ctx.has_value()) { + return EvaluationContext::Merge( + {&global_ctx, &client_ctx, &(*invocation_ctx)}); + } else { + return EvaluationContext::Merge({&global_ctx, &client_ctx}); } - return GetEvaluationContext(); } } // namespace openfeature diff --git a/test/client_api_test.cpp b/test/client_api_test.cpp index a14278a..19b0f50 100644 --- a/test/client_api_test.cpp +++ b/test/client_api_test.cpp @@ -14,8 +14,10 @@ using namespace openfeature; using ::testing::_; +using ::testing::DoAll; using ::testing::NiceMock; using ::testing::Return; +using ::testing::SaveArg; using ::testing::StrictMock; class ClientAPITest : public ::testing::Test { @@ -138,20 +140,87 @@ TEST_F(ClientAPITest, GetObjectValueWithContextReturnsDefault) { } // Test context merging logic indirectly. -TEST_F(ClientAPITest, GetBooleanValueSafeWithMergedContexts) { - EvaluationContext global_ctx = EvaluationContext::Builder().build(); - GlobalContextManager::GetInstance().SetGlobalEvaluationContext(global_ctx); +TEST_F(ClientAPITest, ContextMergingPrecedence) { + GlobalContextManager::GetInstance().SetGlobalEvaluationContext( + EvaluationContext::Builder() + .WithTargetingKey("global-target") + .WithAttribute("global_attr", "global_value") + .WithAttribute("shared_attr_gc", "global_shared_gc") + .WithAttribute("shared_attr_gci", "global_shared_gci") + .build()); - ClientAPI client(repo_, "test-domain"); - EvaluationContext client_ctx = EvaluationContext::Builder().build(); - client.SetEvaluationContext(client_ctx); + std::shared_ptr> mock_provider = + std::make_shared>(); + + EXPECT_CALL(*mock_provider, Init(_)).WillOnce(Return(absl::OkStatus())); + EXPECT_CALL(*mock_provider, Shutdown()).WillOnce(Return(absl::OkStatus())); - EvaluationContext invocation_ctx = EvaluationContext::Builder().build(); + EvaluationContext provider_init_ctx = EvaluationContext::Builder().build(); + repo_.SetProvider("test-domain", mock_provider, provider_init_ctx, true); - // 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)); + ClientAPI client(repo_, "test-domain"); + client.SetEvaluationContext( + EvaluationContext::Builder() + .WithTargetingKey("client-target") + .WithAttribute("client_attr", "client_value") + .WithAttribute("shared_attr_gc", "client_shared_gc") + .WithAttribute("shared_attr_gci", "client_shared_gci") + .build()); + + EvaluationContext invocation_ctx = + EvaluationContext::Builder() + .WithTargetingKey("invocation-target") + .WithAttribute("invocation_attr", "invocation_value") + .WithAttribute("shared_attr_gci", "invocation_shared_gci") + .build(); + + std::string flag_key = "my-test-flag"; + bool default_value = false; + bool expected_value = true; + + EvaluationContext captured_merged_ctx = EvaluationContext::Builder().build(); + + // Expect the provider's GetBooleanEvaluation to be called with the merged + // context. + EXPECT_CALL(*mock_provider, GetBooleanEvaluation(flag_key, default_value, _)) + .WillOnce(DoAll(SaveArg<2>(&captured_merged_ctx), + Return(std::make_unique( + expected_value, Reason::kTargetingMatch, std::nullopt, + FlagMetadata())))); + + // This call will trigger the context merging and evaluation. + EXPECT_EQ(client.GetBooleanValue(flag_key, default_value, invocation_ctx), + expected_value); + + ASSERT_TRUE(captured_merged_ctx.GetTargetingKey().has_value()); + EXPECT_EQ(captured_merged_ctx.GetTargetingKey().value(), "invocation-target"); + + ASSERT_NE(captured_merged_ctx.GetValue("global_attr"), nullptr); + EXPECT_EQ( + std::any_cast(*captured_merged_ctx.GetValue("global_attr")), + "global_value"); + + ASSERT_NE(captured_merged_ctx.GetValue("client_attr"), nullptr); + EXPECT_EQ( + std::any_cast(*captured_merged_ctx.GetValue("client_attr")), + "client_value"); + + ASSERT_NE(captured_merged_ctx.GetValue("invocation_attr"), nullptr); + EXPECT_EQ(std::any_cast( + *captured_merged_ctx.GetValue("invocation_attr")), + "invocation_value"); + + ASSERT_NE(captured_merged_ctx.GetValue("shared_attr_gc"), nullptr); + EXPECT_EQ(std::any_cast( + *captured_merged_ctx.GetValue("shared_attr_gc")), + "client_shared_gc"); + + ASSERT_NE(captured_merged_ctx.GetValue("shared_attr_gci"), nullptr); + EXPECT_EQ(std::any_cast( + *captured_merged_ctx.GetValue("shared_attr_gci")), + "invocation_shared_gci"); + + EXPECT_EQ(captured_merged_ctx.GetAttributes().size(), 5); } // Test behavior when the domain is empty. @@ -160,8 +229,3 @@ TEST_F(ClientAPITest, WorksWithEmptyDomain) { 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