Make Eve command receipts portable
This commit is contained in:
parent
2c4a9adb05
commit
cbe9d9f25e
2 changed files with 270 additions and 1 deletions
|
|
@ -349,6 +349,7 @@ namespace GameCult.Eve.Surface
|
|||
|
||||
[CultDocument("gamecult.eve.command_receipt", SchemaId)]
|
||||
[MessagePackObject]
|
||||
[MessagePackFormatter(typeof(EveCommandReceiptCompatibilityFormatter))]
|
||||
public sealed class EveCommandReceiptDocument
|
||||
{
|
||||
public const string SchemaId = "gamecult.eve.command_receipt.v1";
|
||||
|
|
@ -416,6 +417,206 @@ namespace GameCult.Eve.Surface
|
|||
[Key(12)] public EveSurfaceNavigationTarget? Navigation { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the public command-receipt contract as a string-keyed map while retaining
|
||||
/// read compatibility with the original positional C# representation.
|
||||
/// </summary>
|
||||
public sealed class EveCommandReceiptCompatibilityFormatter :
|
||||
IMessagePackFormatter<EveCommandReceiptDocument?>
|
||||
{
|
||||
private const int LegacyFieldCount = 13;
|
||||
private const int RequiredAndScalarFieldCount = 12;
|
||||
|
||||
public void Serialize(
|
||||
ref MessagePackWriter writer,
|
||||
EveCommandReceiptDocument? value,
|
||||
MessagePackSerializerOptions options)
|
||||
{
|
||||
if (value == null) { writer.WriteNil(); return; }
|
||||
|
||||
writer.WriteMapHeader(value.Navigation == null
|
||||
? RequiredAndScalarFieldCount
|
||||
: RequiredAndScalarFieldCount + 1);
|
||||
Write(ref writer, "schema", value.Schema);
|
||||
Write(ref writer, "receiptId", value.ReceiptId);
|
||||
Write(ref writer, "commandId", value.CommandId);
|
||||
Write(ref writer, "command", value.Command);
|
||||
Write(ref writer, "state", value.State);
|
||||
Write(ref writer, "ownerRepo", value.OwnerRepo);
|
||||
Write(ref writer, "authority", value.Authority);
|
||||
Write(ref writer, "providerId", value.ProviderId);
|
||||
Write(ref writer, "surfaceId", value.SurfaceId);
|
||||
Write(ref writer, "message", value.Message);
|
||||
Write(ref writer, "issuedAtUtc", value.IssuedAtUtc);
|
||||
writer.Write("sourceVersion"); writer.Write(value.SourceVersion);
|
||||
if (value.Navigation != null)
|
||||
{
|
||||
writer.Write("navigation");
|
||||
WriteNavigation(ref writer, value.Navigation);
|
||||
}
|
||||
}
|
||||
|
||||
public EveCommandReceiptDocument? Deserialize(
|
||||
ref MessagePackReader reader,
|
||||
MessagePackSerializerOptions options)
|
||||
{
|
||||
if (reader.TryReadNil()) return null;
|
||||
if (reader.NextMessagePackType == MessagePackType.Map)
|
||||
return ReadMap(ref reader, options);
|
||||
if (reader.NextMessagePackType != MessagePackType.Array)
|
||||
throw new MessagePackSerializationException("Eve command receipt must be a map or legacy array.");
|
||||
|
||||
options.Security.DepthStep(ref reader);
|
||||
try
|
||||
{
|
||||
var fields = reader.ReadArrayHeader();
|
||||
var schema = ReadString(ref reader, fields, 0, EveCommandReceiptDocument.SchemaId);
|
||||
var receiptId = ReadString(ref reader, fields, 1);
|
||||
var commandId = ReadString(ref reader, fields, 2);
|
||||
var command = ReadString(ref reader, fields, 3);
|
||||
var state = ReadString(ref reader, fields, 4);
|
||||
var ownerRepo = ReadString(ref reader, fields, 5);
|
||||
var authority = ReadString(ref reader, fields, 6);
|
||||
var providerId = ReadString(ref reader, fields, 7);
|
||||
var surfaceId = ReadString(ref reader, fields, 8);
|
||||
var message = ReadString(ref reader, fields, 9);
|
||||
var issuedAtUtc = ReadString(ref reader, fields, 10);
|
||||
var sourceVersion = fields > 11 ? reader.ReadInt64() : 0;
|
||||
var navigation = fields > 12 ? ReadNavigation(ref reader, options) : null;
|
||||
for (var index = LegacyFieldCount; index < fields; index++) reader.Skip();
|
||||
return new EveCommandReceiptDocument(
|
||||
schema, receiptId, commandId, command, state, ownerRepo, authority,
|
||||
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation);
|
||||
}
|
||||
finally { reader.Depth--; }
|
||||
}
|
||||
|
||||
private static EveCommandReceiptDocument ReadMap(
|
||||
ref MessagePackReader reader,
|
||||
MessagePackSerializerOptions options)
|
||||
{
|
||||
options.Security.DepthStep(ref reader);
|
||||
try
|
||||
{
|
||||
var schema = EveCommandReceiptDocument.SchemaId;
|
||||
var receiptId = "";
|
||||
var commandId = "";
|
||||
var command = "";
|
||||
var state = "";
|
||||
var ownerRepo = "";
|
||||
var authority = "";
|
||||
var providerId = "";
|
||||
var surfaceId = "";
|
||||
var message = "";
|
||||
var issuedAtUtc = "";
|
||||
long sourceVersion = 0;
|
||||
EveSurfaceNavigationTarget? navigation = null;
|
||||
var fields = reader.ReadMapHeader();
|
||||
for (var index = 0; index < fields; index++)
|
||||
{
|
||||
switch (reader.ReadString())
|
||||
{
|
||||
case "schema": schema = ReadString(ref reader); break;
|
||||
case "receiptId": receiptId = ReadString(ref reader); break;
|
||||
case "commandId": commandId = ReadString(ref reader); break;
|
||||
case "command": command = ReadString(ref reader); break;
|
||||
case "state": state = ReadString(ref reader); break;
|
||||
case "ownerRepo": ownerRepo = ReadString(ref reader); break;
|
||||
case "authority": authority = ReadString(ref reader); break;
|
||||
case "providerId": providerId = ReadString(ref reader); break;
|
||||
case "surfaceId": surfaceId = ReadString(ref reader); break;
|
||||
case "message": message = ReadString(ref reader); break;
|
||||
case "issuedAtUtc": issuedAtUtc = ReadString(ref reader); break;
|
||||
case "sourceVersion": sourceVersion = reader.ReadInt64(); break;
|
||||
case "navigation": navigation = ReadNavigation(ref reader, options); break;
|
||||
default: reader.Skip(); break;
|
||||
}
|
||||
}
|
||||
|
||||
return new EveCommandReceiptDocument(
|
||||
schema, receiptId, commandId, command, state, ownerRepo, authority,
|
||||
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation);
|
||||
}
|
||||
finally { reader.Depth--; }
|
||||
}
|
||||
|
||||
private static void WriteNavigation(
|
||||
ref MessagePackWriter writer,
|
||||
EveSurfaceNavigationTarget? navigation)
|
||||
{
|
||||
if (navigation == null) { writer.WriteNil(); return; }
|
||||
|
||||
writer.WriteMapHeader(5);
|
||||
Write(ref writer, "verseId", navigation.VerseId);
|
||||
Write(ref writer, "providerId", navigation.ProviderId);
|
||||
Write(ref writer, "surfaceId", navigation.SurfaceId);
|
||||
Write(ref writer, "surfaceKind", navigation.SurfaceKind);
|
||||
writer.Write("rendezvousEndpoints");
|
||||
writer.WriteArrayHeader(navigation.RendezvousEndpoints.Length);
|
||||
foreach (var endpoint in navigation.RendezvousEndpoints) writer.Write(endpoint);
|
||||
}
|
||||
|
||||
private static EveSurfaceNavigationTarget? ReadNavigation(
|
||||
ref MessagePackReader reader,
|
||||
MessagePackSerializerOptions options)
|
||||
{
|
||||
if (reader.TryReadNil()) return null;
|
||||
if (reader.NextMessagePackType == MessagePackType.Array)
|
||||
return options.Resolver.GetFormatterWithVerify<EveSurfaceNavigationTarget>()
|
||||
.Deserialize(ref reader, options);
|
||||
if (reader.NextMessagePackType != MessagePackType.Map)
|
||||
throw new MessagePackSerializationException("Eve receipt navigation must be a map or legacy array.");
|
||||
|
||||
options.Security.DepthStep(ref reader);
|
||||
try
|
||||
{
|
||||
var verseId = "";
|
||||
var providerId = "";
|
||||
var surfaceId = "";
|
||||
var surfaceKind = "";
|
||||
var endpoints = Array.Empty<string>();
|
||||
var fields = reader.ReadMapHeader();
|
||||
for (var index = 0; index < fields; index++)
|
||||
{
|
||||
switch (reader.ReadString())
|
||||
{
|
||||
case "verseId": verseId = ReadString(ref reader); break;
|
||||
case "providerId": providerId = ReadString(ref reader); break;
|
||||
case "surfaceId": surfaceId = ReadString(ref reader); break;
|
||||
case "surfaceKind": surfaceKind = ReadString(ref reader); break;
|
||||
case "rendezvousEndpoints": endpoints = ReadStrings(ref reader); break;
|
||||
default: reader.Skip(); break;
|
||||
}
|
||||
}
|
||||
return new EveSurfaceNavigationTarget(verseId, providerId, surfaceId, surfaceKind, endpoints);
|
||||
}
|
||||
finally { reader.Depth--; }
|
||||
}
|
||||
|
||||
private static string[] ReadStrings(ref MessagePackReader reader)
|
||||
{
|
||||
if (reader.TryReadNil()) return Array.Empty<string>();
|
||||
var count = reader.ReadArrayHeader();
|
||||
var values = new string[count];
|
||||
for (var index = 0; index < count; index++) values[index] = ReadString(ref reader);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static string ReadString(ref MessagePackReader reader) => reader.TryReadNil() ? "" : reader.ReadString() ?? "";
|
||||
|
||||
private static string ReadString(
|
||||
ref MessagePackReader reader,
|
||||
int fields,
|
||||
int index,
|
||||
string fallback = "") => fields > index ? ReadString(ref reader) : fallback;
|
||||
|
||||
private static void Write(ref MessagePackWriter writer, string key, string value)
|
||||
{
|
||||
writer.Write(key);
|
||||
writer.Write(value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class EveSurfaceNavigationTarget
|
||||
{
|
||||
|
|
|
|||
|
|
@ -212,9 +212,12 @@ public sealed class EveSurfaceSerializationCompatibilityTests
|
|||
|
||||
var bytes = MessagePackSerializer.Serialize(document, Options);
|
||||
var reader = new MessagePackReader(bytes);
|
||||
using var wireJson = JsonDocument.Parse(MessagePackSerializer.ConvertToJson(bytes));
|
||||
var restored = MessagePackSerializer.Deserialize<EveCommandReceiptDocument>(bytes, Options);
|
||||
|
||||
Assert.That(reader.ReadArrayHeader(), Is.EqualTo(13));
|
||||
Assert.That(reader.NextMessagePackType, Is.EqualTo(MessagePackType.Map));
|
||||
Assert.That(reader.ReadMapHeader(), Is.EqualTo(13));
|
||||
Assert.That(wireJson.RootElement.GetProperty("navigation").ValueKind, Is.EqualTo(JsonValueKind.Object));
|
||||
Assert.That(restored.Navigation, Is.Not.Null);
|
||||
Assert.That(restored.Navigation!.VerseId, Is.EqualTo("gamecult.aetheria"));
|
||||
Assert.That(restored.Navigation.SurfaceId, Is.EqualTo("aetheria.pilot"));
|
||||
|
|
@ -222,6 +225,71 @@ public sealed class EveSurfaceSerializationCompatibilityTests
|
|||
Assert.That(restored.Navigation.RendezvousEndpoints, Is.EqualTo(new[] { "cultnet+tcp://odin.gamecult.example:3076" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CommandReceiptReadsLegacyPositionalRepresentation()
|
||||
{
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
var writer = new MessagePackWriter(buffer);
|
||||
writer.WriteArrayHeader(13);
|
||||
writer.Write(EveCommandReceiptDocument.SchemaId);
|
||||
writer.Write("receipt:legacy");
|
||||
writer.Write("legacy-command");
|
||||
writer.Write("aetheria.hangar.launch");
|
||||
writer.Write("accepted");
|
||||
writer.Write("Aetheria");
|
||||
writer.Write("commander-daemon");
|
||||
writer.Write("aetheria.daemon");
|
||||
writer.Write("aetheria.hangar");
|
||||
writer.Write("");
|
||||
writer.Write("2026-08-17T20:00:00Z");
|
||||
writer.Write(42L);
|
||||
writer.WriteArrayHeader(5);
|
||||
writer.Write("gamecult.aetheria");
|
||||
writer.Write("aetheria.daemon");
|
||||
writer.Write("aetheria.pilot");
|
||||
writer.Write("interactive-world");
|
||||
writer.WriteArrayHeader(1);
|
||||
writer.Write("cultnet+tcp://odin.gamecult.example:3076");
|
||||
writer.Flush();
|
||||
|
||||
var restored = MessagePackSerializer.Deserialize<EveCommandReceiptDocument>(buffer.WrittenMemory, Options);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(restored.ReceiptId, Is.EqualTo("receipt:legacy"));
|
||||
Assert.That(restored.CommandId, Is.EqualTo("legacy-command"));
|
||||
Assert.That(restored.Navigation, Is.Not.Null);
|
||||
Assert.That(restored.Navigation!.SurfaceId, Is.EqualTo("aetheria.pilot"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CommandReceiptOmitsAbsentOptionalNavigation()
|
||||
{
|
||||
var document = new EveCommandReceiptDocument(
|
||||
"receipt:no-navigation",
|
||||
"select-verse",
|
||||
"aetheria.hangar.select_verse",
|
||||
"accepted",
|
||||
"Aetheria",
|
||||
"commander-daemon",
|
||||
"aetheria.daemon",
|
||||
"aetheria.hangar",
|
||||
"",
|
||||
"2026-08-17T20:00:00Z",
|
||||
42);
|
||||
|
||||
var bytes = MessagePackSerializer.Serialize(document, Options);
|
||||
using var wireJson = JsonDocument.Parse(MessagePackSerializer.ConvertToJson(bytes));
|
||||
var restored = MessagePackSerializer.Deserialize<EveCommandReceiptDocument>(bytes, Options);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(wireJson.RootElement.TryGetProperty("navigation", out _), Is.False);
|
||||
Assert.That(restored.Navigation, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
private static EveSurfaceDocument CreateCurrentDocument() => new(
|
||||
"current-provider",
|
||||
"current-kind",
|
||||
|
|
|
|||
Loading…
Reference in a new issue