Skip to content

Commit 79e689c

Browse files
authored
refactor: Implement FDv2 Wire Format + Payload Parser (#514)
This is a pretty straightforward port of `FDv2ChangeSet` and related classes from Java, as well as the protocol objects, such as `PutObject`, `DeleteObject`, etc. * Adds `FDv2Change`, `FDv2ChangeSet` objects for use in data system. * Adds `ServerIntent`, `PutObject`, etc methods for parsing messages from backend. * Adds `tag_invoke` methods based on the existing json deserialization code. * Adds tests for all of the JSON parsing. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces new protocol-level JSON deserialization and data model types that will be used for streaming/data-sync, so schema/compatibility mistakes could affect data updates despite being mostly additive and covered by unit tests. > > **Overview** > Adds initial FDv2 wire-format support by introducing `Selector`, `FDv2Change`, and `FDv2ChangeSet` types for representing full/partial/no-op data updates. > > Implements JSON deserialization (Boost.JSON `tag_invoke`) for FDv2 protocol messages—`IntentCode`, `ServerIntent`, `PutObject`, `DeleteObject`, `PayloadTransferred`, `Goodbye`, and `FDv2Error`—and wires the new parser into the internal build (`serialization/json_fdv2_events.cpp`). > > Adds comprehensive unit tests (`fdv2_serialization_test.cpp`) validating successful parsing, null handling, and schema-failure behavior for missing/invalid fields. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 11be538. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 0edb808 commit 79e689c

6 files changed

Lines changed: 741 additions & 0 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#pragma once
2+
3+
#include <launchdarkly/data_model/flag.hpp>
4+
#include <launchdarkly/data_model/item_descriptor.hpp>
5+
#include <launchdarkly/data_model/segment.hpp>
6+
#include <launchdarkly/data_model/selector.hpp>
7+
8+
#include <string>
9+
#include <variant>
10+
#include <vector>
11+
12+
namespace launchdarkly::data_model {
13+
14+
struct FDv2Change {
15+
std::string key;
16+
std::variant<ItemDescriptor<Flag>, ItemDescriptor<Segment>> object;
17+
};
18+
19+
struct FDv2ChangeSet {
20+
enum class Type {
21+
kFull = 0,
22+
kPartial = 1,
23+
kNone = 2,
24+
};
25+
26+
Type type;
27+
std::vector<FDv2Change> changes;
28+
Selector selector;
29+
};
30+
31+
} // namespace launchdarkly::data_model
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
#include <optional>
5+
#include <string>
6+
7+
namespace launchdarkly::data_model {
8+
9+
/**
10+
* Identifies a specific version of data in the LaunchDarkly backend, used to
11+
* request incremental updates from a known point. <p> A selector is either
12+
* empty or contains a version number and state string that
13+
* were provided by a LaunchDarkly data source. Empty selectors signal that the
14+
* client has no existing data and requires a full payload. <p> <strong>For SDK
15+
* consumers implementing custom data sources:</strong> you should always use
16+
* std::nullopt when constructing a ChangeSet. Non-empty selectors are
17+
* set by LaunchDarkly's own data sources based on state received from the
18+
* LaunchDarkly backend, and are not meaningful when constructed externally.
19+
*/
20+
struct Selector {
21+
struct State {
22+
std::int64_t version;
23+
std::string state;
24+
};
25+
26+
std::optional<State> value;
27+
};
28+
29+
} // namespace launchdarkly::data_model
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#pragma once
2+
3+
#include <launchdarkly/detail/serialization/json_errors.hpp>
4+
5+
#include <boost/json/value.hpp>
6+
#include <tl/expected.hpp>
7+
8+
#include <cstdint>
9+
#include <optional>
10+
#include <string>
11+
#include <vector>
12+
13+
namespace launchdarkly {
14+
15+
enum class IntentCode { kNone, kTransferFull, kTransferChanges, kUnknown };
16+
17+
struct ServerIntentPayload {
18+
std::string id;
19+
std::int64_t target;
20+
IntentCode intent_code;
21+
std::optional<std::string> reason;
22+
};
23+
24+
struct ServerIntent {
25+
std::vector<ServerIntentPayload> payloads;
26+
};
27+
28+
struct PutObject {
29+
std::int64_t version;
30+
std::string kind;
31+
std::string key;
32+
boost::json::value object;
33+
};
34+
35+
struct DeleteObject {
36+
std::int64_t version;
37+
std::string kind;
38+
std::string key;
39+
};
40+
41+
struct PayloadTransferred {
42+
std::string state;
43+
std::int64_t version;
44+
};
45+
46+
struct Goodbye {
47+
std::optional<std::string> reason;
48+
};
49+
50+
struct FDv2Error {
51+
std::optional<std::string> id;
52+
std::string reason;
53+
};
54+
55+
tl::expected<std::optional<IntentCode>, JsonError> tag_invoke(
56+
boost::json::value_to_tag<
57+
tl::expected<std::optional<IntentCode>, JsonError>> const& unused,
58+
boost::json::value const& json_value);
59+
60+
tl::expected<std::optional<ServerIntentPayload>, JsonError> tag_invoke(
61+
boost::json::value_to_tag<tl::expected<std::optional<ServerIntentPayload>,
62+
JsonError>> const& unused,
63+
boost::json::value const& json_value);
64+
65+
tl::expected<std::optional<ServerIntent>, JsonError> tag_invoke(
66+
boost::json::value_to_tag<
67+
tl::expected<std::optional<ServerIntent>, JsonError>> const& unused,
68+
boost::json::value const& json_value);
69+
70+
tl::expected<std::optional<PutObject>, JsonError> tag_invoke(
71+
boost::json::value_to_tag<
72+
tl::expected<std::optional<PutObject>, JsonError>> const& unused,
73+
boost::json::value const& json_value);
74+
75+
tl::expected<std::optional<DeleteObject>, JsonError> tag_invoke(
76+
boost::json::value_to_tag<
77+
tl::expected<std::optional<DeleteObject>, JsonError>> const& unused,
78+
boost::json::value const& json_value);
79+
80+
tl::expected<std::optional<PayloadTransferred>, JsonError> tag_invoke(
81+
boost::json::value_to_tag<tl::expected<std::optional<PayloadTransferred>,
82+
JsonError>> const& unused,
83+
boost::json::value const& json_value);
84+
85+
tl::expected<std::optional<Goodbye>, JsonError> tag_invoke(
86+
boost::json::value_to_tag<
87+
tl::expected<std::optional<Goodbye>, JsonError>> const& unused,
88+
boost::json::value const& json_value);
89+
90+
tl::expected<std::optional<FDv2Error>, JsonError> tag_invoke(
91+
boost::json::value_to_tag<
92+
tl::expected<std::optional<FDv2Error>, JsonError>> const& unused,
93+
boost::json::value const& json_value);
94+
95+
} // namespace launchdarkly

libs/internal/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ set(INTERNAL_SOURCES
3434
serialization/json_evaluation_reason.cpp
3535
serialization/value_mapping.cpp
3636
serialization/json_evaluation_result.cpp
37+
serialization/json_fdv2_events.cpp
3738
serialization/json_sdk_data_set.cpp
3839
serialization/json_segment.cpp
3940
serialization/json_primitives.cpp
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#include <boost/core/ignore_unused.hpp>
2+
#include <boost/json.hpp>
3+
#include <launchdarkly/serialization/json_fdv2_events.hpp>
4+
#include <launchdarkly/serialization/value_mapping.hpp>
5+
#include <tl/expected.hpp>
6+
7+
namespace launchdarkly {
8+
9+
tl::expected<std::optional<IntentCode>, JsonError> tag_invoke(
10+
boost::json::value_to_tag<
11+
tl::expected<std::optional<IntentCode>, JsonError>> const& unused,
12+
boost::json::value const& json_value) {
13+
boost::ignore_unused(unused);
14+
15+
REQUIRE_STRING(json_value);
16+
17+
auto const& str = json_value.as_string();
18+
if (str == "none") {
19+
return IntentCode::kNone;
20+
} else if (str == "xfer-full") {
21+
return IntentCode::kTransferFull;
22+
} else if (str == "xfer-changes") {
23+
return IntentCode::kTransferChanges;
24+
} else {
25+
return IntentCode::kUnknown;
26+
}
27+
}
28+
29+
tl::expected<std::optional<ServerIntentPayload>, JsonError> tag_invoke(
30+
boost::json::value_to_tag<tl::expected<std::optional<ServerIntentPayload>,
31+
JsonError>> const& unused,
32+
boost::json::value const& json_value) {
33+
boost::ignore_unused(unused);
34+
35+
REQUIRE_OBJECT(json_value);
36+
auto const& obj = json_value.as_object();
37+
38+
ServerIntentPayload payload{};
39+
40+
PARSE_REQUIRED_FIELD(payload.id, obj, "id");
41+
PARSE_REQUIRED_FIELD(payload.target, obj, "target");
42+
PARSE_REQUIRED_FIELD(payload.intent_code, obj, "intentCode");
43+
PARSE_CONDITIONAL_FIELD(payload.reason, obj, "reason");
44+
45+
return payload;
46+
}
47+
48+
tl::expected<std::optional<ServerIntent>, JsonError> tag_invoke(
49+
boost::json::value_to_tag<
50+
tl::expected<std::optional<ServerIntent>, JsonError>> const& unused,
51+
boost::json::value const& json_value) {
52+
boost::ignore_unused(unused);
53+
54+
REQUIRE_OBJECT(json_value);
55+
auto const& obj = json_value.as_object();
56+
57+
ServerIntent intent{};
58+
59+
PARSE_REQUIRED_FIELD(intent.payloads, obj, "payloads");
60+
61+
return intent;
62+
}
63+
64+
tl::expected<std::optional<PutObject>, JsonError> tag_invoke(
65+
boost::json::value_to_tag<
66+
tl::expected<std::optional<PutObject>, JsonError>> const& unused,
67+
boost::json::value const& json_value) {
68+
boost::ignore_unused(unused);
69+
70+
REQUIRE_OBJECT(json_value);
71+
auto const& obj = json_value.as_object();
72+
73+
PutObject put{};
74+
75+
PARSE_REQUIRED_FIELD(put.version, obj, "version");
76+
PARSE_REQUIRED_FIELD(put.kind, obj, "kind");
77+
PARSE_REQUIRED_FIELD(put.key, obj, "key");
78+
79+
auto const& it = obj.find("object");
80+
if (it == obj.end()) {
81+
return tl::make_unexpected(JsonError::kSchemaFailure);
82+
}
83+
put.object = it->value();
84+
85+
return put;
86+
}
87+
88+
tl::expected<std::optional<DeleteObject>, JsonError> tag_invoke(
89+
boost::json::value_to_tag<
90+
tl::expected<std::optional<DeleteObject>, JsonError>> const& unused,
91+
boost::json::value const& json_value) {
92+
boost::ignore_unused(unused);
93+
94+
REQUIRE_OBJECT(json_value);
95+
auto const& obj = json_value.as_object();
96+
97+
DeleteObject del{};
98+
99+
PARSE_REQUIRED_FIELD(del.version, obj, "version");
100+
PARSE_REQUIRED_FIELD(del.kind, obj, "kind");
101+
PARSE_REQUIRED_FIELD(del.key, obj, "key");
102+
103+
return del;
104+
}
105+
106+
tl::expected<std::optional<PayloadTransferred>, JsonError> tag_invoke(
107+
boost::json::value_to_tag<tl::expected<std::optional<PayloadTransferred>,
108+
JsonError>> const& unused,
109+
boost::json::value const& json_value) {
110+
boost::ignore_unused(unused);
111+
112+
REQUIRE_OBJECT(json_value);
113+
auto const& obj = json_value.as_object();
114+
115+
PayloadTransferred transferred{};
116+
117+
PARSE_REQUIRED_FIELD(transferred.state, obj, "state");
118+
PARSE_REQUIRED_FIELD(transferred.version, obj, "version");
119+
120+
return transferred;
121+
}
122+
123+
tl::expected<std::optional<Goodbye>, JsonError> tag_invoke(
124+
boost::json::value_to_tag<
125+
tl::expected<std::optional<Goodbye>, JsonError>> const& unused,
126+
boost::json::value const& json_value) {
127+
boost::ignore_unused(unused);
128+
129+
REQUIRE_OBJECT(json_value);
130+
auto const& obj = json_value.as_object();
131+
132+
Goodbye goodbye{};
133+
134+
PARSE_CONDITIONAL_FIELD(goodbye.reason, obj, "reason");
135+
136+
return goodbye;
137+
}
138+
139+
tl::expected<std::optional<FDv2Error>, JsonError> tag_invoke(
140+
boost::json::value_to_tag<
141+
tl::expected<std::optional<FDv2Error>, JsonError>> const& unused,
142+
boost::json::value const& json_value) {
143+
boost::ignore_unused(unused);
144+
145+
REQUIRE_OBJECT(json_value);
146+
auto const& obj = json_value.as_object();
147+
148+
FDv2Error error{};
149+
150+
PARSE_REQUIRED_FIELD(error.reason, obj, "reason");
151+
PARSE_CONDITIONAL_FIELD(error.id, obj, "id");
152+
153+
return error;
154+
}
155+
156+
} // namespace launchdarkly

0 commit comments

Comments
 (0)