GameCult.Thing.Contract: every empty memory is an empty RecordPayload; wire nil is refused on read
Present(ReadOnlyMemory<byte>) is deleted. The constructor copies any view, default included, as an empty payload; a nil on the wire (which decodes to a default view) is noted by the constructor and refused in OnAfterDeserialize, so the nil-positions rows hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
686b32831b
commit
e25311a0c0
3 changed files with 43 additions and 15 deletions
|
|
@ -104,16 +104,32 @@ public sealed class CommandDecl
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A typed record as its MessagePack bytes. Every empty memory (<c>ReadOnlyMemory<byte>.Empty</c>, a default view,
|
||||
/// an empty array) is an empty payload, as Rust's empty <c>Vec<u8></c> is. A nil on the wire is not an empty
|
||||
/// payload: it decodes to a default view the constructor cannot tell from an empty one, so the constructor notes it and
|
||||
/// the decode door refuses it once the value is read.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public sealed class RecordPayload
|
||||
public sealed class RecordPayload : IMessagePackSerializationCallbackReceiver
|
||||
{
|
||||
private readonly bool _viewWasDefault;
|
||||
|
||||
[Key(0)] public string Schema { get; }
|
||||
[Key(1)] public ReadOnlyMemory<byte> Bytes { get; }
|
||||
|
||||
public RecordPayload(string schema, ReadOnlyMemory<byte> bytes)
|
||||
{
|
||||
Schema = ThingDomain.Present(schema, "Schema");
|
||||
Bytes = ThingDomain.Present(bytes, "Bytes").ToArray();
|
||||
_viewWasDefault = bytes.Equals(default(ReadOnlyMemory<byte>));
|
||||
Bytes = bytes.ToArray();
|
||||
}
|
||||
|
||||
void IMessagePackSerializationCallbackReceiver.OnBeforeSerialize() { }
|
||||
|
||||
void IMessagePackSerializationCallbackReceiver.OnAfterDeserialize()
|
||||
{
|
||||
if (_viewWasDefault) throw new ThingDomainException("Bytes", "present");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,15 +49,6 @@ public static class ThingDomain
|
|||
public static T Present<T>(T? value, string slot) where T : class =>
|
||||
value ?? throw new ThingDomainException(slot, "present");
|
||||
|
||||
/// <summary>
|
||||
/// A required byte slot. A null on the wire decodes to a default view with no backing store, so the default view is
|
||||
/// refused as nil; an empty array is a present, empty payload.
|
||||
/// </summary>
|
||||
public static ReadOnlyMemory<byte> Present(ReadOnlyMemory<byte> value, string slot) =>
|
||||
value.Equals(default(ReadOnlyMemory<byte>))
|
||||
? throw new ThingDomainException(slot, "present")
|
||||
: value;
|
||||
|
||||
/// <summary>
|
||||
/// A required list slot: a read-only copy, so the caller's list cannot change the value afterwards. A null list or
|
||||
/// a null item is refused naming the slot.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using MessagePack;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
|
||||
|
|
@ -165,11 +166,31 @@ public sealed class ReadOnlyListTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void ANilPayloadIsRefusedAndAnEmptyOneIsKeptThroughTheWire()
|
||||
public void EveryEmptyMemoryIsAnEmptyPayloadThatWritesAsRustsEmptyVec()
|
||||
{
|
||||
var error = Assert.Throws<ThingDomainException>(() => new RecordPayload("s", default))!;
|
||||
Assert.That(error.Domain, Is.EqualTo("Bytes"));
|
||||
Assert.That(error.Bound, Is.EqualTo("present"));
|
||||
// Rust's empty Vec<u8> is bin8 of length 0 (c4 00); a RecordPayload is [schema, bytes].
|
||||
var rust = new byte[] { 0x92, 0xA1, (byte)'s', 0xC4, 0x00 };
|
||||
var empties = new Dictionary<string, ReadOnlyMemory<byte>>
|
||||
{
|
||||
["ReadOnlyMemory.Empty"] = ReadOnlyMemory<byte>.Empty,
|
||||
["Memory.Empty"] = Memory<byte>.Empty,
|
||||
["default"] = default(ReadOnlyMemory<byte>),
|
||||
["Array.Empty"] = Array.Empty<byte>(),
|
||||
["new byte[0]"] = new byte[0],
|
||||
};
|
||||
foreach (var (name, memory) in empties)
|
||||
{
|
||||
var payload = new RecordPayload("s", memory);
|
||||
Assert.That(payload.Bytes.Length, Is.EqualTo(0), name);
|
||||
Assert.That(Wire.Write(payload), Is.EqualTo(rust), name);
|
||||
Assert.That(Wire.Write(Wire.Read(typeof(RecordPayload), rust)), Is.EqualTo(rust), name);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ANilPayloadOnTheWireIsRefusedAndAnEmptyOneIsKeptThroughTheWire()
|
||||
{
|
||||
Assert.Throws<MessagePackSerializationException>(() => Wire.Read(typeof(RecordPayload), new byte[] { 0x92, 0xA1, (byte)'s', 0xC0 }));
|
||||
|
||||
var empty = new CommandInvocation("i", "k", 1, "go", new RecordPayload("s", Array.Empty<byte>()), new List<CapturedInput>(), "c", "t", null);
|
||||
var decoded = (CommandInvocation)Wire.Read(typeof(CommandInvocation), Wire.Write(empty));
|
||||
|
|
|
|||
Loading…
Reference in a new issue