CultMathJson writes CultPhasor as cosine/sine and still reads cos/sin
Fields takes an optional legacy name per field, matched on read only and mapped to the same index as the current name, so a legacy and a current name in one object are the same property and the duplicate rules decide (refused when AllowDuplicateProperties is false, last wins otherwise). Writing never emits a legacy name. MessagePack bytes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c027d3f884
commit
58baccce78
3 changed files with 69 additions and 7 deletions
|
|
@ -14,7 +14,7 @@ namespace GameCult.Caching.MessagePack;
|
|||
/// The canonical System.Text.Json shape of every public CultMath value type, the JSON twin of
|
||||
/// <see cref="CultMathResolver"/>. Vectors, quaternions and colors are objects keyed by their
|
||||
/// field names (<c>{"x":1,"y":2}</c>), matrices are arrays of row objects, <c>rect</c> is
|
||||
/// <c>{"min":..,"max":..}</c>, <c>CultCellular</c> is <c>{"nearest":..,"edge":..,"id":..}</c>, <c>CultPhasor</c> is <c>{"cos":..,"sin":..}</c>, <c>SpanNoise</c> is <c>{"field":..,"curvature":..,"fold":..}</c>, <c>SpanPath</c> is <c>{"midpoint":..,"velocity":..,"speed":..,"acceleration":..,"halfLength":..}</c>, <c>SpanStage</c> is <c>{"frequency":..,"stretch":..,"axis":..,"shift":..,"offset":..,"octaveStep":..}</c>, and <c>CultSiteCells</c> is <c>{"nearest":..,"second":..,"nearestSite":..,"secondSite":..}</c>.
|
||||
/// <c>{"min":..,"max":..}</c>, <c>CultCellular</c> is <c>{"nearest":..,"edge":..,"id":..}</c>, <c>CultPhasor</c> is <c>{"cosine":..,"sine":..}</c> (reading also accepts the legacy <c>cos</c>/<c>sin</c> names), <c>SpanNoise</c> is <c>{"field":..,"curvature":..,"fold":..}</c>, <c>SpanPath</c> is <c>{"midpoint":..,"velocity":..,"speed":..,"acceleration":..,"halfLength":..}</c>, <c>SpanStage</c> is <c>{"frequency":..,"stretch":..,"axis":..,"shift":..,"offset":..,"octaveStep":..}</c>, and <c>CultSiteCells</c> is <c>{"nearest":..,"second":..,"nearestSite":..,"secondSite":..}</c>.
|
||||
/// Property names are fixed: <c>PropertyNamingPolicy</c> does not rename them, and
|
||||
/// <c>PropertyNameCaseInsensitive</c> makes reading case-insensitive.
|
||||
/// <para>
|
||||
|
|
@ -270,14 +270,24 @@ public static class CultMathJson
|
|||
{
|
||||
private readonly string[] names;
|
||||
private readonly byte[][] utf8Names;
|
||||
private readonly string?[] legacyNames;
|
||||
private readonly byte[]?[] legacyUtf8Names;
|
||||
private readonly JsonEncodedText[] encoded;
|
||||
private readonly Node<S>[] children;
|
||||
private readonly int[] offsets;
|
||||
|
||||
public Fields(params (string Name, Node<S> Child)[] fields)
|
||||
: this(Array.ConvertAll(fields, f => (f.Name, f.Child, (string?)null)))
|
||||
{
|
||||
}
|
||||
|
||||
// A legacy name is matched on read only and maps to the same field as its current name.
|
||||
public Fields(params (string Name, Node<S> Child, string? Legacy)[] fields)
|
||||
{
|
||||
names = new string[fields.Length];
|
||||
utf8Names = new byte[fields.Length][];
|
||||
legacyNames = new string?[fields.Length];
|
||||
legacyUtf8Names = new byte[]?[fields.Length];
|
||||
encoded = new JsonEncodedText[fields.Length];
|
||||
children = new Node<S>[fields.Length];
|
||||
offsets = new int[fields.Length];
|
||||
|
|
@ -285,6 +295,8 @@ public static class CultMathJson
|
|||
{
|
||||
names[i] = fields[i].Name;
|
||||
utf8Names[i] = Encoding.UTF8.GetBytes(fields[i].Name);
|
||||
legacyNames[i] = fields[i].Legacy;
|
||||
legacyUtf8Names[i] = fields[i].Legacy is { } legacy ? Encoding.UTF8.GetBytes(legacy) : null;
|
||||
encoded[i] = JsonEncodedText.Encode(fields[i].Name);
|
||||
children[i] = fields[i].Child;
|
||||
offsets[i] = Count;
|
||||
|
|
@ -339,13 +351,14 @@ public static class CultMathJson
|
|||
if (!ignoreCase)
|
||||
{
|
||||
for (var i = 0; i < utf8Names.Length; i++)
|
||||
if (r.ValueTextEquals(utf8Names[i])) return i;
|
||||
if (r.ValueTextEquals(utf8Names[i]) || (legacyUtf8Names[i] is { } legacy && r.ValueTextEquals(legacy))) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
var name = r.GetString()!;
|
||||
for (var i = 0; i < names.Length; i++)
|
||||
if (string.Equals(names[i], name, StringComparison.OrdinalIgnoreCase)) return i;
|
||||
if (string.Equals(names[i], name, StringComparison.OrdinalIgnoreCase)
|
||||
|| (legacyNames[i] is { } legacy && string.Equals(legacy, name, StringComparison.OrdinalIgnoreCase))) return i;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -481,7 +494,7 @@ public static class CultMathJson
|
|||
Add<Random, uint>(UIntScalar.Instance, new Fields<uint>(("state", Val<uint>())),
|
||||
a => new Random { state = a[0] }, (v, a) => a[0] = v.state),
|
||||
|
||||
// Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cos, sin}, SpanNoise is {field, curvature, fold}, SpanPath is {midpoint, velocity, speed, acceleration, halfLength}, SpanStage is {frequency, stretch, axis, shift, offset, octaveStep}, CultSiteCells is {nearest, second, nearestSite, secondSite}.
|
||||
// Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cosine, sine}, SpanNoise is {field, curvature, fold}, SpanPath is {midpoint, velocity, speed, acceleration, halfLength}, SpanStage is {frequency, stretch, axis, shift, offset, octaveStep}, CultSiteCells is {nearest, second, nearestSite, secondSite}.
|
||||
Add<float2x2, float>(f, new Rows<float>(2, Xy<float>()),
|
||||
a => new float2x2(a[0], a[1], a[2], a[3]),
|
||||
(v, a) => { a[0] = v[0].x; a[1] = v[0].y; a[2] = v[1].x; a[3] = v[1].y; }),
|
||||
|
|
@ -507,7 +520,7 @@ public static class CultMathJson
|
|||
a[4] = v.edge.x; a[5] = v.edge.y; a[6] = v.edge.z; a[7] = v.edge.w;
|
||||
a[8] = v.id;
|
||||
}),
|
||||
Add<CultPhasor, float>(f, new Fields<float>(("cos", Xyzw<float>()), ("sin", Xyzw<float>())),
|
||||
Add<CultPhasor, float>(f, new Fields<float>(("cosine", Xyzw<float>(), "cos"), ("sine", Xyzw<float>(), "sin")),
|
||||
a => new CultPhasor(new float4(a[0], a[1], a[2], a[3]), new float4(a[4], a[5], a[6], a[7])),
|
||||
(v, a) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ public sealed class CultMathResolver : IFormatterResolver
|
|||
return value;
|
||||
}),
|
||||
|
||||
// Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cos, sin], SpanNoise is [field, curvature, fold], SpanPath is [midpoint, velocity, speed, acceleration, halfLength], SpanStage is [frequency, stretch, axis, shift, offset, octaveStep], CultSiteCells is [nearest, second, nearestSite, secondSite].
|
||||
// Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cosine, sine], SpanNoise is [field, curvature, fold], SpanPath is [midpoint, velocity, speed, acceleration, halfLength], SpanStage is [frequency, stretch, axis, shift, offset, octaveStep], CultSiteCells is [nearest, second, nearestSite, secondSite].
|
||||
Shape<float2x2>((ref MessagePackWriter w, float2x2 v) => { w.WriteArrayHeader(2); PutFloat2(ref w, v[0]); PutFloat2(ref w, v[1]); },
|
||||
(ref MessagePackReader r) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ namespace GameCult.Caching.Tests
|
|||
Json(new CultCellular(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f), 0.25f)).Should().Be(
|
||||
"""{"nearest":{"x":1,"y":2,"z":3,"w":4},"edge":{"x":5,"y":6,"z":7,"w":8},"id":0.25}""");
|
||||
Json(new CultPhasor(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f))).Should().Be(
|
||||
"""{"cos":{"x":1,"y":2,"z":3,"w":4},"sin":{"x":5,"y":6,"z":7,"w":8}}""");
|
||||
"""{"cosine":{"x":1,"y":2,"z":3,"w":4},"sine":{"x":5,"y":6,"z":7,"w":8}}""");
|
||||
Json(new SpanNoise(new float4(1f, 2f, 3f, 4f), 0.25f, 0.5f)).Should().Be(
|
||||
"""{"field":{"x":1,"y":2,"z":3,"w":4},"curvature":0.25,"fold":0.5}""");
|
||||
Json(new SpanPath { midpoint = new float3(1f, 2f, 3f), velocity = new float3(4f, 5f, 6f), speed = 7f, acceleration = 8f, halfLength = 9f }).Should().Be(
|
||||
|
|
@ -92,6 +92,55 @@ namespace GameCult.Caching.Tests
|
|||
Parse<float2?>("null").Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_AcceptsTheLegacyPhasorNames()
|
||||
{
|
||||
// The fixture is what the 0.4.1-era encoder wrote: keys cos/sin.
|
||||
var path = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Fixtures", "cultphasor-json-legacy.json");
|
||||
var legacy = Parse<CultPhasor[]>(System.IO.File.ReadAllText(path));
|
||||
var current = new[]
|
||||
{
|
||||
new CultPhasor(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f)),
|
||||
math.phacelle(new float3(0.3f, 1.7f, -2.2f), new float3(6.2831853f, 0f, 0f), 0.25f, 0.5f),
|
||||
};
|
||||
legacy.Should().HaveCount(2);
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
BitsOf(legacy[i]).Should().Equal(BitsOf(current[i]));
|
||||
BitsOf(Parse<CultPhasor>(Json(current[i]))).Should().Equal(BitsOf(current[i]));
|
||||
}
|
||||
|
||||
var caseInsensitive = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }.AddCultMathConverters();
|
||||
BitsOf(JsonSerializer.Deserialize<CultPhasor>("""{"COS":{"x":1,"y":2,"z":3,"w":4},"Sin":{"x":5,"y":6,"z":7,"w":8}}""", caseInsensitive))
|
||||
.Should().Equal(BitsOf(current[0]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_RefusesALegacyAndCurrentNameTogether()
|
||||
{
|
||||
var property = typeof(JsonSerializerOptions).GetProperty("AllowDuplicateProperties");
|
||||
Assert.That(property, Is.Not.Null, "the test runtime's System.Text.Json has AllowDuplicateProperties");
|
||||
var strict = new JsonSerializerOptions().AddCultMathConverters();
|
||||
property!.SetValue(strict, false);
|
||||
Action both = () => JsonSerializer.Deserialize<CultPhasor>("""{"cos":{"x":1},"cosine":{"x":2}}""", strict);
|
||||
both.Should().Throw<JsonException>().WithMessage("*appears more than once*");
|
||||
Action reversed = () => JsonSerializer.Deserialize<CultPhasor>("""{"sine":{"x":1},"sin":{"x":2}}""", strict);
|
||||
reversed.Should().Throw<JsonException>().WithMessage("*appears more than once*");
|
||||
|
||||
// With duplicates allowed the later property replaces the earlier one, as for any repeated name.
|
||||
Parse<CultPhasor>("""{"cos":{"x":1,"y":9},"cosine":{"x":2}}""").cosine.Should().Be(new float4(2f, 0f, 0f, 0f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Writing_NeverEmitsLegacyNames()
|
||||
{
|
||||
using var document = JsonDocument.Parse(Json(new CultPhasor(new float4(1f, 1f, 1f, 1f), new float4(2f, 2f, 2f, 2f))));
|
||||
document.RootElement.EnumerateObject().Select(p => p.Name).Should().Equal("cosine", "sine");
|
||||
}
|
||||
|
||||
private static int[] BitsOf(CultPhasor v) => new[] { v.cosine.x, v.cosine.y, v.cosine.z, v.cosine.w, v.sine.x, v.sine.y, v.sine.z, v.sine.w }
|
||||
.Select(BitConverter.SingleToInt32Bits).ToArray();
|
||||
|
||||
[Test]
|
||||
public void Reading_RefusesShapesThatAreNotTheType()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue