diff --git a/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp b/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp index 836dc20b4..f176cf403 100644 --- a/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp +++ b/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include + #include namespace launchdarkly::data_model { @@ -46,16 +48,18 @@ struct ContextAwareReference< std::is_same::value>::type> { using fields = FieldNames; - std::string contextKind; + ContextKind contextKind; AttributeReference reference; }; +// NOLINTBEGIN cppcoreguidelines-macro-usage #define DEFINE_CONTEXT_KIND_FIELD(name) \ - std::string name; \ + ContextKind name; \ constexpr static const char* kContextFieldName = #name; #define DEFINE_ATTRIBUTE_REFERENCE_FIELD(name) \ AttributeReference name; \ constexpr static const char* kReferenceFieldName = #name; +// NOLINTEND cppcoreguidelines-macro-usage } // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/context_kind.hpp b/libs/internal/include/launchdarkly/data_model/context_kind.hpp new file mode 100644 index 000000000..efa5b342e --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/context_kind.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include + +namespace launchdarkly::data_model { + +BOOST_STRONG_TYPEDEF(std::string, ContextKind); + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp index 282dd4bad..65d56d12a 100644 --- a/libs/internal/include/launchdarkly/data_model/flag.hpp +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -15,8 +16,6 @@ namespace launchdarkly::data_model { struct Flag { - using ContextKind = std::string; - struct Rollout { enum class Kind { kUnrecognized = 0, diff --git a/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp b/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp index 77b56af90..c9eac9038 100644 --- a/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -25,15 +26,10 @@ tl::expected, JsonError> tag_invoke( auto const& obj = json_value.as_object(); - std::optional kind; + std::optional kind; PARSE_CONDITIONAL_FIELD(kind, obj, Type::fields::kContextFieldName); - if (kind && *kind == "") { - // Empty string is not a valid kind. - return tl::make_unexpected(JsonError::kSchemaFailure); - } - std::string attr_ref_or_name; PARSE_FIELD_DEFAULT(attr_ref_or_name, obj, Type::fields::kReferenceFieldName, "key"); @@ -43,7 +39,8 @@ tl::expected, JsonError> tag_invoke( AttributeReference::FromReferenceStr(attr_ref_or_name)}; } - return Type{"user", AttributeReference::FromLiteralStr(attr_ref_or_name)}; + return Type{data_model::ContextKind("user"), + AttributeReference::FromLiteralStr(attr_ref_or_name)}; } } // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp b/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp new file mode 100644 index 000000000..c7046a00b --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include +#include + +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +} // namespace launchdarkly diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index 99b4c9dd3..e61c03098 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -39,6 +39,7 @@ add_library(${LIBNAME} OBJECT serialization/json_primitives.cpp serialization/json_rule_clause.cpp serialization/json_flag.cpp + serialization/json_context_kind.cpp encoding/base_64.cpp encoding/sha_256.cpp signals/boost_signal_connection.cpp) diff --git a/libs/internal/src/serialization/json_context_kind.cpp b/libs/internal/src/serialization/json_context_kind.cpp new file mode 100644 index 000000000..96f8e6114 --- /dev/null +++ b/libs/internal/src/serialization/json_context_kind.cpp @@ -0,0 +1,24 @@ +#include +#include + +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_STRING(json_value); + auto const& str = json_value.as_string(); + + if (str.empty()) { + /* Empty string is not a valid context kind. */ + return tl::make_unexpected(JsonError::kSchemaFailure); + } + + return data_model::ContextKind(str.c_str()); +} +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index 4dd40e92e..f25d48a8d 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -100,7 +101,8 @@ tl::expected, JsonError> tag_invoke( data_model::Flag::Target target; PARSE_FIELD(target.values, obj, "values"); PARSE_FIELD(target.variation, obj, "variation"); - PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", "user"); + PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", + data_model::ContextKind("user")); return target; } diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index 43bf8eaa4..8e1feb5ee 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -7,6 +7,7 @@ #include using namespace launchdarkly; +using launchdarkly::data_model::ContextKind; TEST(SDKDataSetTests, DeserializesEmptyDataSet) { auto result = @@ -87,7 +88,7 @@ TEST(SegmentRuleTests, DeserializesSimpleAttributeReference) { tl::expected>(boost::json::parse( R"({"rolloutContextKind" : "foo", "bucketBy" : "bar", "clauses": []})")); ASSERT_TRUE(result); - ASSERT_EQ(result->rolloutContextKind, "foo"); + ASSERT_EQ(result->rolloutContextKind, ContextKind("foo")); ASSERT_EQ(result->bucketBy, AttributeReference("bar")); } @@ -96,7 +97,7 @@ TEST(SegmentRuleTests, DeserializesPointerAttributeReference) { tl::expected>(boost::json::parse( R"({"rolloutContextKind" : "foo", "bucketBy" : "/foo/bar", "clauses": []})")); ASSERT_TRUE(result); - ASSERT_EQ(result->rolloutContextKind, "foo"); + ASSERT_EQ(result->rolloutContextKind, ContextKind("foo")); ASSERT_EQ(result->bucketBy, AttributeReference("/foo/bar")); } @@ -105,10 +106,17 @@ TEST(SegmentRuleTests, DeserializesEscapedReference) { tl::expected>(boost::json::parse( R"({"rolloutContextKind" : "foo", "bucketBy" : "/~1foo~1bar", "clauses": []})")); ASSERT_TRUE(result); - ASSERT_EQ(result->rolloutContextKind, "foo"); + ASSERT_EQ(result->rolloutContextKind, ContextKind("foo")); ASSERT_EQ(result->bucketBy, AttributeReference("/~1foo~1bar")); } +TEST(SegmentRuleTests, RejectsEmptyContextKind) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"rolloutContextKind" : "", "bucketBy" : "/~1foo~1bar", "clauses": []})")); + ASSERT_FALSE(result); +} + TEST(SegmentRuleTests, DeserializesLiteralAttributeName) { auto result = boost::json::value_to< tl::expected>( @@ -176,6 +184,13 @@ TEST(ClauseTests, DeserializesEscapedReference) { ASSERT_EQ(result->attribute, AttributeReference("/~1foo~1bar")); } +TEST(ClauseTests, RejectsEmptyContextKind) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "/~1foo~1bar", "op": "in", "values": ["a"], "contextKind" : ""})")); + ASSERT_FALSE(result); +} + TEST(ClauseTests, DeserializesLiteralAttributeName) { auto result = boost::json::value_to>( @@ -192,7 +207,7 @@ TEST(RolloutTests, DeserializesMinimumValid) { boost::json::parse(R"({})")); ASSERT_TRUE(result); ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kRollout); - ASSERT_EQ(result->contextKind, "user"); + ASSERT_EQ(result->contextKind, ContextKind("user")); ASSERT_EQ(result->bucketBy, "key"); } @@ -202,7 +217,7 @@ TEST(RolloutTests, DeserializesAllFieldsWithAttributeReference) { R"({"kind": "experiment", "contextKind": "org", "bucketBy": "/foo/bar", "seed" : 123, "variations" : []})")); ASSERT_TRUE(result); ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kExperiment); - ASSERT_EQ(result->contextKind, "org"); + ASSERT_EQ(result->contextKind, ContextKind("org")); ASSERT_EQ(result->bucketBy, "/foo/bar"); ASSERT_EQ(result->seed, 123); ASSERT_TRUE(result->variations.empty()); @@ -214,7 +229,7 @@ TEST(RolloutTests, DeserializesAllFieldsWithLiteralAttributeName) { R"({"kind": "experiment", "bucketBy": "/foo/bar", "seed" : 123, "variations" : []})")); ASSERT_TRUE(result); ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kExperiment); - ASSERT_EQ(result->contextKind, "user"); + ASSERT_EQ(result->contextKind, ContextKind("user")); ASSERT_EQ(result->bucketBy, "/~1foo~1bar"); ASSERT_EQ(result->seed, 123); ASSERT_TRUE(result->variations.empty()); @@ -278,7 +293,7 @@ TEST(TargetTests, DeserializesMinimumValid) { tl::expected>( boost::json::parse(R"({})")); ASSERT_TRUE(result); - ASSERT_EQ(result->contextKind, "user"); + ASSERT_EQ(result->contextKind, ContextKind("user")); ASSERT_EQ(result->variation, 0); ASSERT_TRUE(result->values.empty()); } @@ -295,7 +310,7 @@ TEST(TargetTests, DeserializesAllFields) { tl::expected>(boost::json::parse( R"({"variation" : 123, "values" : ["a"], "contextKind" : "org"})")); ASSERT_TRUE(result); - ASSERT_EQ(result->contextKind, "org"); + ASSERT_EQ(result->contextKind, ContextKind("org")); ASSERT_EQ(result->variation, 123); ASSERT_EQ(result->values.size(), 1); ASSERT_EQ(result->values[0], "a"); diff --git a/libs/server-sdk/tests/dependency_tracker_test.cpp b/libs/server-sdk/tests/dependency_tracker_test.cpp index 3bfacb643..bbffbc09b 100644 --- a/libs/server-sdk/tests/dependency_tracker_test.cpp +++ b/libs/server-sdk/tests/dependency_tracker_test.cpp @@ -13,6 +13,7 @@ using launchdarkly::server_side::data_store::SegmentDescriptor; using launchdarkly::AttributeReference; using launchdarkly::Value; using launchdarkly::data_model::Clause; +using launchdarkly::data_model::ContextKind; using launchdarkly::data_model::Flag; using launchdarkly::data_model::ItemDescriptor; using launchdarkly::data_model::Segment; @@ -197,7 +198,7 @@ TEST(DependencyTrackerTest, UsesSegmentRulesToCalculateDependencies) { flag_a.rules.push_back(Flag::Rule{std::vector{ Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, - "user", AttributeReference()}}}); + ContextKind("user"), AttributeReference()}}}); tracker.UpdateDependencies("flagA", FlagDescriptor(flag_a)); tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); @@ -231,7 +232,7 @@ TEST(DependencyTrackerTest, TracksSegmentDependencyOfPrerequisite) { flag_a.rules.push_back(Flag::Rule{std::vector{ Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, - "", AttributeReference()}}}); + ContextKind(""), AttributeReference()}}}); flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); @@ -270,8 +271,8 @@ TEST(DependencyTrackerTest, HandlesSegmentsDependentOnOtherSegments) { segment_b.rules.push_back(Segment::Rule{ std::vector{Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, - "user", AttributeReference()}}, - std::nullopt, std::nullopt, "", AttributeReference()}); + ContextKind("user"), AttributeReference()}}, + std::nullopt, std::nullopt, ContextKind(""), AttributeReference()}); tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); tracker.UpdateDependencies("segmentB", SegmentDescriptor(segment_b));