Skip to content

Commit 10b2f4a

Browse files
authored
chore: Add FDv2 payload types. (#183)
Adds payload types and support for JSON serialization/deserialization of those types. These types are not used yet. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces FDv2 payload types with custom JSON converters and an event wrapper for partial deserialization, plus comprehensive tests. > > - **Internal backend (FDv2 payloads)**: > - Add models: `ServerIntent`, `ServerIntentPayload`, `PutObject`, `DeleteObject`, `PayloadTransferred`, `Error`, `Goodbye`. > - Implement `JsonConverter`s for each type with required-field validation. > - Add `FDv2PollEvent` wrapper and `FDv2PollEventConverter` supporting lazy (`JsonElement`) data with typed accessors (`AsServerIntent`, `AsPutObject`, etc.). > - **Tests**: > - Add unit tests covering round-trip serialization/deserialization, missing-field errors, and end-to-end parsing of a full polling response (flags and segments). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 110ebb3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 21cfe9f commit 10b2f4a

File tree

8 files changed

+1631
-0
lines changed

8 files changed

+1631
-0
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
using static LaunchDarkly.Sdk.Internal.JsonConverterHelpers;
5+
6+
namespace LaunchDarkly.Sdk.Server.Internal.FDv2Payloads
7+
{
8+
/// <summary>
9+
/// Represents the delete-object event, which contains a payload object that should be deleted.
10+
/// </summary>
11+
internal sealed class DeleteObject
12+
{
13+
/// <summary>
14+
/// The minimum payload version this change applies to. May not match the target version of ServerIntent message.
15+
/// </summary>
16+
public int Version { get; }
17+
18+
/// <summary>
19+
/// The kind of the object being deleted ("flag" or "segment").
20+
/// <para>
21+
/// This field is required and will never be null.
22+
/// </para>
23+
/// </summary>
24+
public string Kind { get; }
25+
26+
/// <summary>
27+
/// The identifier of the object.
28+
/// <para>
29+
/// This field is required and will never be null.
30+
/// </para>
31+
/// </summary>
32+
public string Key { get; }
33+
34+
/// <summary>
35+
/// Constructs a new DeleteObject.
36+
/// </summary>
37+
/// <param name="version">The minimum payload version this change applies to.</param>
38+
/// <param name="kind">The kind of object being deleted ("flag" or "segment").</param>
39+
/// <param name="key">The identifier of the object.</param>
40+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="kind"/> or <paramref name="key"/> is null.</exception>
41+
public DeleteObject(int version, string kind, string key)
42+
{
43+
Version = version;
44+
Kind = kind ?? throw new ArgumentNullException(nameof(kind));
45+
Key = key ?? throw new ArgumentNullException(nameof(key));
46+
}
47+
}
48+
49+
/// <summary>
50+
/// JsonConverter for DeleteObject events.
51+
/// </summary>
52+
internal sealed class DeleteObjectConverter : JsonConverter<DeleteObject>
53+
{
54+
private const string AttributeVersion = "version";
55+
private const string AttributeKind = "kind";
56+
private const string AttributeKey = "key";
57+
58+
internal static readonly DeleteObjectConverter Instance = new DeleteObjectConverter();
59+
60+
private static readonly string[] RequiredProperties =
61+
{ AttributeVersion, AttributeKind, AttributeKey };
62+
63+
public override DeleteObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
64+
{
65+
var version = 0;
66+
string kind = null;
67+
string key = null;
68+
69+
for (var obj = RequireObject(ref reader).WithRequiredProperties(RequiredProperties); obj.Next(ref reader);)
70+
{
71+
switch (obj.Name)
72+
{
73+
case AttributeVersion:
74+
version = reader.GetInt32();
75+
break;
76+
case AttributeKind:
77+
kind = reader.GetString();
78+
break;
79+
case AttributeKey:
80+
key = reader.GetString();
81+
break;
82+
default:
83+
reader.Skip();
84+
break;
85+
}
86+
}
87+
88+
return new DeleteObject(version, kind, key);
89+
}
90+
91+
public override void Write(Utf8JsonWriter writer, DeleteObject value, JsonSerializerOptions options)
92+
{
93+
writer.WriteStartObject();
94+
writer.WriteNumber(AttributeVersion, value.Version);
95+
writer.WriteString(AttributeKind, value.Kind);
96+
writer.WriteString(AttributeKey, value.Key);
97+
writer.WriteEndObject();
98+
}
99+
}
100+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
using static LaunchDarkly.Sdk.Internal.JsonConverterHelpers;
5+
6+
namespace LaunchDarkly.Sdk.Server.Internal.FDv2Payloads
7+
{
8+
/// <summary>
9+
/// Represents the error event, which indicates an error encountered server-side affecting the payload transfer.
10+
/// SDKs must discard partially transferred data. The SDK remains connected and expects the server to recover.
11+
/// </summary>
12+
internal sealed class Error
13+
{
14+
/// <summary>
15+
/// The unique string identifier of the entity the error relates to.
16+
/// </summary>
17+
public string Id { get; }
18+
19+
/// <summary>
20+
/// Human-readable reason the error occurred.
21+
/// <para>
22+
/// This field is required and will never be null.
23+
/// </para>
24+
/// </summary>
25+
public string Reason { get; }
26+
27+
/// <summary>
28+
/// Constructs a new Error.
29+
/// </summary>
30+
/// <param name="id">The unique string identifier of the entity the error relates to.</param>
31+
/// <param name="reason">Human-readable reason the error occurred.</param>
32+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="reason"/> is null.</exception>
33+
public Error(string id, string reason)
34+
{
35+
Id = id;
36+
Reason = reason ?? throw new ArgumentNullException(nameof(reason));
37+
}
38+
}
39+
40+
/// <summary>
41+
/// JsonConverter for Error events.
42+
/// </summary>
43+
internal sealed class ErrorConverter : JsonConverter<Error>
44+
{
45+
private const string AttributeId = "id";
46+
private const string AttributeReason = "reason";
47+
48+
internal static readonly ErrorConverter Instance = new ErrorConverter();
49+
private static readonly string[] RequiredProperties = { AttributeReason };
50+
51+
public override Error Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
52+
{
53+
string id = null;
54+
string reason = null;
55+
56+
for (var obj = RequireObject(ref reader).WithRequiredProperties(RequiredProperties); obj.Next(ref reader);)
57+
{
58+
switch (obj.Name)
59+
{
60+
case AttributeId:
61+
id = reader.GetString();
62+
break;
63+
case AttributeReason:
64+
reason = reader.GetString();
65+
break;
66+
default:
67+
reader.Skip();
68+
break;
69+
}
70+
}
71+
72+
return new Error(id, reason);
73+
}
74+
75+
public override void Write(Utf8JsonWriter writer, Error value, JsonSerializerOptions options)
76+
{
77+
writer.WriteStartObject();
78+
if (value.Id != null)
79+
{
80+
writer.WriteString(AttributeId, value.Id);
81+
}
82+
83+
writer.WriteString(AttributeReason, value.Reason);
84+
writer.WriteEndObject();
85+
}
86+
}
87+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using System;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
using static LaunchDarkly.Sdk.Internal.JsonConverterHelpers;
5+
6+
namespace LaunchDarkly.Sdk.Server.Internal.FDv2Payloads
7+
{
8+
/// <summary>
9+
/// Represents a FDv2 polling event with partial deserialization.
10+
/// The event type is deserialized, but the data is kept as a JsonElement for lazy deserialization.
11+
/// </summary>
12+
internal sealed class FDv2PollEvent
13+
{
14+
/// <summary>
15+
/// The event type string (e.g., "server-intent", "put-object", "delete-object", etc.).
16+
/// </summary>
17+
public string EventType { get; }
18+
19+
/// <summary>
20+
/// The raw JSON element representing the event data.
21+
/// This should be deserialized based on the EventType.
22+
/// </summary>
23+
public JsonElement Data { get; }
24+
25+
public FDv2PollEvent(string eventType, JsonElement data)
26+
{
27+
EventType = eventType;
28+
Data = data;
29+
}
30+
31+
/// <summary>
32+
/// Deserializes the Data element as a ServerIntent.
33+
/// </summary>
34+
public ServerIntent AsServerIntent()
35+
{
36+
return JsonSerializer.Deserialize<ServerIntent>(Data.GetRawText(), GetSerializerOptions());
37+
}
38+
39+
/// <summary>
40+
/// Deserializes the Data element as a PutObject.
41+
/// </summary>
42+
public PutObject AsPutObject()
43+
{
44+
return JsonSerializer.Deserialize<PutObject>(Data.GetRawText(), GetSerializerOptions());
45+
}
46+
47+
/// <summary>
48+
/// Deserializes the Data element as a DeleteObject.
49+
/// </summary>
50+
public DeleteObject AsDeleteObject()
51+
{
52+
return JsonSerializer.Deserialize<DeleteObject>(Data.GetRawText(), GetSerializerOptions());
53+
}
54+
55+
/// <summary>
56+
/// Deserializes the Data element as a PayloadTransferred.
57+
/// </summary>
58+
public PayloadTransferred AsPayloadTransferred()
59+
{
60+
return JsonSerializer.Deserialize<PayloadTransferred>(Data.GetRawText(), GetSerializerOptions());
61+
}
62+
63+
/// <summary>
64+
/// Deserializes the Data element as an Error.
65+
/// </summary>
66+
public Error AsError()
67+
{
68+
return JsonSerializer.Deserialize<Error>(Data.GetRawText(), GetSerializerOptions());
69+
}
70+
71+
/// <summary>
72+
/// Deserializes the Data element as a Goodbye.
73+
/// </summary>
74+
public Goodbye AsGoodbye()
75+
{
76+
return JsonSerializer.Deserialize<Goodbye>(Data.GetRawText(), GetSerializerOptions());
77+
}
78+
79+
private static JsonSerializerOptions GetSerializerOptions()
80+
{
81+
var options = new JsonSerializerOptions();
82+
options.Converters.Add(ServerIntentConverter.Instance);
83+
options.Converters.Add(PutObjectConverter.Instance);
84+
options.Converters.Add(DeleteObjectConverter.Instance);
85+
options.Converters.Add(PayloadTransferredConverter.Instance);
86+
options.Converters.Add(ErrorConverter.Instance);
87+
options.Converters.Add(GoodbyeConverter.Instance);
88+
return options;
89+
}
90+
}
91+
92+
/// <summary>
93+
/// JsonConverter for FDv2PollEvent wrapper with partial deserialization.
94+
/// </summary>
95+
internal sealed class FDv2PollEventConverter : JsonConverter<FDv2PollEvent>
96+
{
97+
private const string AttributeEvent = "event";
98+
private const string AttributeData = "data";
99+
100+
internal static readonly FDv2PollEventConverter Instance = new FDv2PollEventConverter();
101+
private static readonly string[] RequiredProperties = new string[] { AttributeEvent, AttributeData };
102+
103+
public override FDv2PollEvent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
104+
{
105+
string eventType = null;
106+
JsonElement data = default;
107+
108+
for (var obj = RequireObject(ref reader).WithRequiredProperties(RequiredProperties); obj.Next(ref reader);)
109+
{
110+
switch (obj.Name)
111+
{
112+
case AttributeEvent:
113+
eventType = reader.GetString();
114+
break;
115+
case AttributeData:
116+
// Store the raw JSON element for later deserialization based on the event type
117+
data = JsonElement.ParseValue(ref reader);
118+
break;
119+
default:
120+
reader.Skip();
121+
break;
122+
}
123+
}
124+
125+
return new FDv2PollEvent(eventType, data);
126+
}
127+
128+
public override void Write(Utf8JsonWriter writer, FDv2PollEvent value, JsonSerializerOptions options)
129+
{
130+
writer.WriteStartObject();
131+
if (value.EventType != null)
132+
{
133+
writer.WriteString(AttributeEvent, value.EventType);
134+
}
135+
136+
writer.WritePropertyName(AttributeData);
137+
value.Data.WriteTo(writer);
138+
writer.WriteEndObject();
139+
}
140+
}
141+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
using static LaunchDarkly.Sdk.Internal.JsonConverterHelpers;
5+
6+
namespace LaunchDarkly.Sdk.Server.Internal.FDv2Payloads
7+
{
8+
/// <summary>
9+
/// Represents the goodbye event, which indicates that the server is about to disconnect.
10+
/// </summary>
11+
internal sealed class Goodbye
12+
{
13+
/// <summary>
14+
/// Reason for the disconnection.
15+
/// </summary>
16+
public string Reason { get; }
17+
18+
/// <summary>
19+
/// Constructs a new Goodbye.
20+
/// </summary>
21+
/// <param name="reason">Reason for the disconnection.</param>
22+
public Goodbye(string reason)
23+
{
24+
Reason = reason;
25+
}
26+
}
27+
28+
/// <summary>
29+
/// JsonConverter for Goodbye events.
30+
/// </summary>
31+
internal sealed class GoodbyeConverter : JsonConverter<Goodbye>
32+
{
33+
private const string AttributeReason = "reason";
34+
35+
internal static readonly GoodbyeConverter Instance = new GoodbyeConverter();
36+
37+
public override Goodbye Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
38+
{
39+
string reason = null;
40+
41+
for (var obj = RequireObject(ref reader); obj.Next(ref reader);)
42+
{
43+
switch (obj.Name)
44+
{
45+
case AttributeReason:
46+
reason = reader.GetString();
47+
break;
48+
default:
49+
reader.Skip();
50+
break;
51+
}
52+
}
53+
54+
return new Goodbye(reason);
55+
}
56+
57+
public override void Write(Utf8JsonWriter writer, Goodbye value, JsonSerializerOptions options)
58+
{
59+
writer.WriteStartObject();
60+
if (value.Reason != null)
61+
{
62+
writer.WriteString(AttributeReason, value.Reason);
63+
}
64+
65+
writer.WriteEndObject();
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)