From 1bb6805762afbf8004db41ee4ba0dc0b0e18a55c Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Thu, 26 Mar 2026 11:47:43 -0700 Subject: [PATCH 1/2] feat: Add FDv2 wire format types and JSON deserializers --- .../launchdarkly/data_model/fdv2_change.hpp | 31 ++ .../launchdarkly/data_model/selector.hpp | 29 ++ .../serialization/json_fdv2_events.hpp | 97 ++++ libs/internal/src/CMakeLists.txt | 1 + .../src/serialization/json_fdv2_events.cpp | 158 +++++++ .../tests/fdv2_serialization_test.cpp | 434 ++++++++++++++++++ 6 files changed, 750 insertions(+) create mode 100644 libs/internal/include/launchdarkly/data_model/fdv2_change.hpp create mode 100644 libs/internal/include/launchdarkly/data_model/selector.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_fdv2_events.hpp create mode 100644 libs/internal/src/serialization/json_fdv2_events.cpp create mode 100644 libs/internal/tests/fdv2_serialization_test.cpp diff --git a/libs/internal/include/launchdarkly/data_model/fdv2_change.hpp b/libs/internal/include/launchdarkly/data_model/fdv2_change.hpp new file mode 100644 index 000000000..0d45470aa --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/fdv2_change.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace launchdarkly::data_model { + +struct FDv2Change { + std::string key; + std::variant, ItemDescriptor> object; +}; + +struct FDv2ChangeSet { + enum class Type { + kFull = 0, + kPartial = 1, + kNone = 2, + }; + + Type type; + std::vector changes; + Selector selector; +}; + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/selector.hpp b/libs/internal/include/launchdarkly/data_model/selector.hpp new file mode 100644 index 000000000..ae4478567 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/selector.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +namespace launchdarkly::data_model { + +/** + * Identifies a specific version of data in the LaunchDarkly backend, used to + * request incremental updates from a known point.

A selector is either + * empty or contains a version number and state string that + * were provided by a LaunchDarkly data source. Empty selectors signal that the + * client has no existing data and requires a full payload.

For SDK + * consumers implementing custom data sources: you should always use + * std::nullopt when constructing a ChangeSet. Non-empty selectors are + * set by LaunchDarkly's own data sources based on state received from the + * LaunchDarkly backend, and are not meaningful when constructed externally. + */ +struct Selector { + struct State { + std::int64_t version; + std::string state; + }; + + std::optional value; +}; + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/serialization/json_fdv2_events.hpp b/libs/internal/include/launchdarkly/serialization/json_fdv2_events.hpp new file mode 100644 index 000000000..c58b6534b --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_fdv2_events.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly { + +enum class IntentCode { kNone, kTransferFull, kTransferChanges, kUnknown }; + +struct ServerIntentPayload { + std::string id; + std::int64_t target; + IntentCode intent_code; + std::optional reason; +}; + +struct ServerIntent { + std::vector payloads; +}; + +struct PutObject { + std::int64_t version; + std::string kind; + std::string key; + boost::json::value object; +}; + +struct DeleteObject { + std::int64_t version; + std::string kind; + std::string key; +}; + +struct PayloadTransferred { + std::string state; + std::int64_t version; +}; + +struct Goodbye { + std::optional reason; +}; + +struct FDv2Error { + std::optional id; + std::string reason; +}; + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +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 3a1b9e171..44600638b 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -34,6 +34,7 @@ set(INTERNAL_SOURCES serialization/json_evaluation_reason.cpp serialization/value_mapping.cpp serialization/json_evaluation_result.cpp + serialization/json_fdv2_events.cpp serialization/json_sdk_data_set.cpp serialization/json_segment.cpp serialization/json_primitives.cpp diff --git a/libs/internal/src/serialization/json_fdv2_events.cpp b/libs/internal/src/serialization/json_fdv2_events.cpp new file mode 100644 index 000000000..4fec11754 --- /dev/null +++ b/libs/internal/src/serialization/json_fdv2_events.cpp @@ -0,0 +1,158 @@ +#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) { + boost::ignore_unused(unused); + + REQUIRE_STRING(json_value); + + auto const& str = json_value.as_string(); + if (str == "none") { + return IntentCode::kNone; + } else if (str == "xfer-full") { + return IntentCode::kTransferFull; + } else if (str == "xfer-changes") { + return IntentCode::kTransferChanges; + } else { + return IntentCode::kUnknown; + } +} + +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_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + ServerIntentPayload payload{}; + + PARSE_REQUIRED_FIELD(payload.id, obj, "id"); + PARSE_REQUIRED_FIELD(payload.target, obj, "target"); + PARSE_REQUIRED_FIELD(payload.intent_code, obj, "intentCode"); + PARSE_CONDITIONAL_FIELD(payload.reason, obj, "reason"); + + return payload; +} + +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_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + ServerIntent intent{}; + + PARSE_REQUIRED_FIELD(intent.payloads, obj, "payloads"); + + return intent; +} + +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_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + PutObject put{}; + + PARSE_REQUIRED_FIELD(put.version, obj, "version"); + PARSE_REQUIRED_FIELD(put.kind, obj, "kind"); + PARSE_REQUIRED_FIELD(put.key, obj, "key"); + + auto const& it = obj.find("object"); + if (it == obj.end()) { + return tl::make_unexpected(JsonError::kSchemaFailure); + } + put.object = it->value(); + + return put; +} + +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_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + DeleteObject del{}; + + PARSE_REQUIRED_FIELD(del.version, obj, "version"); + PARSE_REQUIRED_FIELD(del.kind, obj, "kind"); + PARSE_REQUIRED_FIELD(del.key, obj, "key"); + + return del; +} + +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_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + PayloadTransferred transferred{}; + + PARSE_REQUIRED_FIELD(transferred.state, obj, "state"); + PARSE_REQUIRED_FIELD(transferred.version, obj, "version"); + + return transferred; +} + +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_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + Goodbye goodbye{}; + + PARSE_CONDITIONAL_FIELD(goodbye.reason, obj, "reason"); + + return goodbye; +} + +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_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + FDv2Error error{}; + + PARSE_REQUIRED_FIELD(error.reason, obj, "reason"); + PARSE_CONDITIONAL_FIELD(error.id, obj, "id"); + + return error; +} + +} // namespace launchdarkly diff --git a/libs/internal/tests/fdv2_serialization_test.cpp b/libs/internal/tests/fdv2_serialization_test.cpp new file mode 100644 index 000000000..500039759 --- /dev/null +++ b/libs/internal/tests/fdv2_serialization_test.cpp @@ -0,0 +1,434 @@ +#include + +#include +#include + +using namespace launchdarkly; + +// ============================================================================ +// IntentCode +// ============================================================================ + +TEST(IntentCodeTests, DeserializesNone) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"("none")")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(*result.value(), IntentCode::kNone); +} + +TEST(IntentCodeTests, DeserializesXferFull) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"("xfer-full")")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(*result.value(), IntentCode::kTransferFull); +} + +TEST(IntentCodeTests, DeserializesXferChanges) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"("xfer-changes")")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(*result.value(), IntentCode::kTransferChanges); +} + +TEST(IntentCodeTests, UnknownStringDeserializesToUnknown) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"("bogus")")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(*result.value(), IntentCode::kUnknown); +} + +TEST(IntentCodeTests, WrongTypeReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"(42)")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(IntentCodeTests, NullReturnsNullopt) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"(null)")); + ASSERT_TRUE(result); + ASSERT_FALSE(result.value()); +} + +// ============================================================================ +// ServerIntentPayload +// ============================================================================ + +TEST(ServerIntentPayloadTests, DeserializesValidPayload) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse( + R"({"id":"abc","target":1,"intentCode":"xfer-full","reason":"initial"})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(result.value()->id, "abc"); + ASSERT_EQ(result.value()->target, std::int64_t{1}); + ASSERT_EQ(result.value()->intent_code, IntentCode::kTransferFull); + ASSERT_TRUE(result.value()->reason); + ASSERT_EQ(*result.value()->reason, "initial"); +} + +TEST(ServerIntentPayloadTests, WrongTypeReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"("not-an-object")")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(ServerIntentPayloadTests, MissingIdReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse( + R"({"target":1,"intentCode":"none","reason":"r"})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(ServerIntentPayloadTests, MissingIntentCodeReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"id":"x","target":1,"reason":"r"})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(ServerIntentPayloadTests, MissingTargetReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"id":"x","intentCode":"none","reason":"r"})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(ServerIntentPayloadTests, MissingReasonSucceeds) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"id":"x","target":1,"intentCode":"none"})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_FALSE(result.value()->reason); +} + +TEST(ServerIntentPayloadTests, NullReturnsNullopt) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"(null)")); + ASSERT_TRUE(result); + ASSERT_FALSE(result.value()); +} + +// ============================================================================ +// ServerIntent +// ============================================================================ + +TEST(ServerIntentTests, DeserializesValidIntent) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse( + R"({"payloads":[{"id":"abc","target":1,"intentCode":"xfer-full","reason":"r"}]})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(result.value()->payloads.size(), 1u); + ASSERT_EQ(result.value()->payloads[0].id, "abc"); +} + +TEST(ServerIntentTests, DeserializesEmptyPayloads) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"payloads":[]})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_TRUE(result.value()->payloads.empty()); +} + +TEST(ServerIntentTests, MissingPayloadsReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(ServerIntentTests, WrongTypeReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"([])")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(ServerIntentTests, NullReturnsNullopt) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"(null)")); + ASSERT_TRUE(result); + ASSERT_FALSE(result.value()); +} + +// ============================================================================ +// PutObject +// ============================================================================ + +TEST(PutObjectTests, DeserializesValidPutObject) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse( + R"({"version":5,"kind":"flag","key":"my-flag","object":{"on":true}})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(result.value()->version, std::int64_t{5}); + ASSERT_EQ(result.value()->kind, "flag"); + ASSERT_EQ(result.value()->key, "my-flag"); + ASSERT_TRUE(result.value()->object.is_object()); +} + +TEST(PutObjectTests, MissingObjectReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"version":1,"kind":"flag","key":"k"})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(PutObjectTests, MissingVersionReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse( + R"({"kind":"flag","key":"k","object":{}})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(PutObjectTests, MissingKindReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"version":1,"key":"k","object":{}})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(PutObjectTests, MissingKeyReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"version":1,"kind":"flag","object":{}})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(PutObjectTests, WrongTypeReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"("string")")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(PutObjectTests, NullReturnsNullopt) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"(null)")); + ASSERT_TRUE(result); + ASSERT_FALSE(result.value()); +} + +// ============================================================================ +// DeleteObject +// ============================================================================ + +TEST(DeleteObjectTests, DeserializesValidDeleteObject) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse( + R"({"version":3,"kind":"segment","key":"my-seg"})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(result.value()->version, std::int64_t{3}); + ASSERT_EQ(result.value()->kind, "segment"); + ASSERT_EQ(result.value()->key, "my-seg"); +} + +TEST(DeleteObjectTests, MissingVersionReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"kind":"flag","key":"k"})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(DeleteObjectTests, MissingKindReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"version":1,"key":"k"})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(DeleteObjectTests, MissingKeyReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"version":1,"kind":"flag"})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(DeleteObjectTests, WrongTypeReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"([1,2,3])")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(DeleteObjectTests, NullReturnsNullopt) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"(null)")); + ASSERT_TRUE(result); + ASSERT_FALSE(result.value()); +} + +// ============================================================================ +// PayloadTransferred +// ============================================================================ + +TEST(PayloadTransferredTests, DeserializesValidPayload) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"json({"state":"(p:abc:42)","version":42})json")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(result.value()->state, std::string("(p:abc:42)")); + ASSERT_EQ(result.value()->version, std::int64_t{42}); +} + +TEST(PayloadTransferredTests, MissingStateReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"version":1})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(PayloadTransferredTests, MissingVersionReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"state":"s"})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(PayloadTransferredTests, WrongTypeReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"("string")")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(PayloadTransferredTests, NullReturnsNullopt) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"(null)")); + ASSERT_TRUE(result); + ASSERT_FALSE(result.value()); +} + +// ============================================================================ +// Goodbye +// ============================================================================ + +TEST(GoodbyeTests, DeserializesWithReason) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"reason":"shutting down"})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_TRUE(result.value()->reason); + ASSERT_EQ(*result.value()->reason, "shutting down"); +} + +TEST(GoodbyeTests, DeserializesWithoutReason) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_FALSE(result.value()->reason); +} + +TEST(GoodbyeTests, WrongTypeReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"("goodbye")")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(GoodbyeTests, NullReturnsNullopt) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"(null)")); + ASSERT_TRUE(result); + ASSERT_FALSE(result.value()); +} + +// ============================================================================ +// FDv2Error +// ============================================================================ + +TEST(FDv2ErrorTests, DeserializesWithRequiredFieldOnly) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"reason":"something went wrong"})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(result.value()->reason, "something went wrong"); + ASSERT_FALSE(result.value()->id); +} + +TEST(FDv2ErrorTests, DeserializesWithAllFields) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"reason":"oops","id":"err-42"})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + ASSERT_EQ(result.value()->reason, "oops"); + ASSERT_TRUE(result.value()->id); + ASSERT_EQ(*result.value()->id, "err-42"); +} + +TEST(FDv2ErrorTests, MissingReasonReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"id":"e1"})")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(FDv2ErrorTests, WrongTypeReturnsSchemaFailure) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"([])")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(FDv2ErrorTests, NullReturnsNullopt) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"(null)")); + ASSERT_TRUE(result); + ASSERT_FALSE(result.value()); +} From c57eabbe6fd10cd2a5ffb8994436b7eaf81cb8d4 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Thu, 26 Mar 2026 12:01:25 -0700 Subject: [PATCH 2/2] run clang-format --- .../serialization/json_fdv2_events.hpp | 10 ++--- .../src/serialization/json_fdv2_events.cpp | 10 ++--- .../tests/fdv2_serialization_test.cpp | 43 ++++++++----------- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/libs/internal/include/launchdarkly/serialization/json_fdv2_events.hpp b/libs/internal/include/launchdarkly/serialization/json_fdv2_events.hpp index c58b6534b..50ea455ad 100644 --- a/libs/internal/include/launchdarkly/serialization/json_fdv2_events.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_fdv2_events.hpp @@ -58,9 +58,8 @@ tl::expected, JsonError> tag_invoke( boost::json::value const& json_value); tl::expected, JsonError> tag_invoke( - boost::json::value_to_tag< - tl::expected, JsonError>> const& - unused, + boost::json::value_to_tag, + JsonError>> const& unused, boost::json::value const& json_value); tl::expected, JsonError> tag_invoke( @@ -79,9 +78,8 @@ tl::expected, JsonError> tag_invoke( boost::json::value const& json_value); tl::expected, JsonError> tag_invoke( - boost::json::value_to_tag< - tl::expected, JsonError>> const& - unused, + boost::json::value_to_tag, + JsonError>> const& unused, boost::json::value const& json_value); tl::expected, JsonError> tag_invoke( diff --git a/libs/internal/src/serialization/json_fdv2_events.cpp b/libs/internal/src/serialization/json_fdv2_events.cpp index 4fec11754..63fba0d38 100644 --- a/libs/internal/src/serialization/json_fdv2_events.cpp +++ b/libs/internal/src/serialization/json_fdv2_events.cpp @@ -27,9 +27,8 @@ tl::expected, JsonError> tag_invoke( } tl::expected, JsonError> tag_invoke( - boost::json::value_to_tag< - tl::expected, JsonError>> const& - unused, + boost::json::value_to_tag, + JsonError>> const& unused, boost::json::value const& json_value) { boost::ignore_unused(unused); @@ -105,9 +104,8 @@ tl::expected, JsonError> tag_invoke( } tl::expected, JsonError> tag_invoke( - boost::json::value_to_tag< - tl::expected, JsonError>> const& - unused, + boost::json::value_to_tag, + JsonError>> const& unused, boost::json::value const& json_value) { boost::ignore_unused(unused); diff --git a/libs/internal/tests/fdv2_serialization_test.cpp b/libs/internal/tests/fdv2_serialization_test.cpp index 500039759..8083fa03b 100644 --- a/libs/internal/tests/fdv2_serialization_test.cpp +++ b/libs/internal/tests/fdv2_serialization_test.cpp @@ -90,8 +90,7 @@ TEST(ServerIntentPayloadTests, WrongTypeReturnsSchemaFailure) { TEST(ServerIntentPayloadTests, MissingIdReturnsSchemaFailure) { auto result = boost::json::value_to< tl::expected, JsonError>>( - boost::json::parse( - R"({"target":1,"intentCode":"none","reason":"r"})")); + boost::json::parse(R"({"target":1,"intentCode":"none","reason":"r"})")); ASSERT_FALSE(result); ASSERT_EQ(result.error(), JsonError::kSchemaFailure); } @@ -135,9 +134,8 @@ TEST(ServerIntentPayloadTests, NullReturnsNullopt) { TEST(ServerIntentTests, DeserializesValidIntent) { auto result = boost::json::value_to< - tl::expected, JsonError>>( - boost::json::parse( - R"({"payloads":[{"id":"abc","target":1,"intentCode":"xfer-full","reason":"r"}]})")); + tl::expected, JsonError>>(boost::json::parse( + R"({"payloads":[{"id":"abc","target":1,"intentCode":"xfer-full","reason":"r"}]})")); ASSERT_TRUE(result); ASSERT_TRUE(result.value()); ASSERT_EQ(result.value()->payloads.size(), 1u); @@ -183,9 +181,8 @@ TEST(ServerIntentTests, NullReturnsNullopt) { TEST(PutObjectTests, DeserializesValidPutObject) { auto result = boost::json::value_to< - tl::expected, JsonError>>( - boost::json::parse( - R"({"version":5,"kind":"flag","key":"my-flag","object":{"on":true}})")); + tl::expected, JsonError>>(boost::json::parse( + R"({"version":5,"kind":"flag","key":"my-flag","object":{"on":true}})")); ASSERT_TRUE(result); ASSERT_TRUE(result.value()); ASSERT_EQ(result.value()->version, std::int64_t{5}); @@ -205,8 +202,7 @@ TEST(PutObjectTests, MissingObjectReturnsSchemaFailure) { TEST(PutObjectTests, MissingVersionReturnsSchemaFailure) { auto result = boost::json::value_to< tl::expected, JsonError>>( - boost::json::parse( - R"({"kind":"flag","key":"k","object":{}})")); + boost::json::parse(R"({"kind":"flag","key":"k","object":{}})")); ASSERT_FALSE(result); ASSERT_EQ(result.error(), JsonError::kSchemaFailure); } @@ -250,8 +246,7 @@ TEST(PutObjectTests, NullReturnsNullopt) { TEST(DeleteObjectTests, DeserializesValidDeleteObject) { auto result = boost::json::value_to< tl::expected, JsonError>>( - boost::json::parse( - R"({"version":3,"kind":"segment","key":"my-seg"})")); + boost::json::parse(R"({"version":3,"kind":"segment","key":"my-seg"})")); ASSERT_TRUE(result); ASSERT_TRUE(result.value()); ASSERT_EQ(result.value()->version, std::int64_t{3}); @@ -350,9 +345,9 @@ TEST(PayloadTransferredTests, NullReturnsNullopt) { // ============================================================================ TEST(GoodbyeTests, DeserializesWithReason) { - auto result = boost::json::value_to< - tl::expected, JsonError>>( - boost::json::parse(R"({"reason":"shutting down"})")); + auto result = + boost::json::value_to, JsonError>>( + boost::json::parse(R"({"reason":"shutting down"})")); ASSERT_TRUE(result); ASSERT_TRUE(result.value()); ASSERT_TRUE(result.value()->reason); @@ -360,26 +355,26 @@ TEST(GoodbyeTests, DeserializesWithReason) { } TEST(GoodbyeTests, DeserializesWithoutReason) { - auto result = boost::json::value_to< - tl::expected, JsonError>>( - boost::json::parse(R"({})")); + auto result = + boost::json::value_to, JsonError>>( + boost::json::parse(R"({})")); ASSERT_TRUE(result); ASSERT_TRUE(result.value()); ASSERT_FALSE(result.value()->reason); } TEST(GoodbyeTests, WrongTypeReturnsSchemaFailure) { - auto result = boost::json::value_to< - tl::expected, JsonError>>( - boost::json::parse(R"("goodbye")")); + auto result = + boost::json::value_to, JsonError>>( + boost::json::parse(R"("goodbye")")); ASSERT_FALSE(result); ASSERT_EQ(result.error(), JsonError::kSchemaFailure); } TEST(GoodbyeTests, NullReturnsNullopt) { - auto result = boost::json::value_to< - tl::expected, JsonError>>( - boost::json::parse(R"(null)")); + auto result = + boost::json::value_to, JsonError>>( + boost::json::parse(R"(null)")); ASSERT_TRUE(result); ASSERT_FALSE(result.value()); }