feat(commons): AttributeValueCodec for canonical list value encode/decode

This commit is contained in:
Joseph Doherty
2026-06-16 15:21:56 -04:00
parent 70fa0e7397
commit 8bd8079a7f
2 changed files with 260 additions and 0 deletions
@@ -0,0 +1,161 @@
using System.Globalization;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;
/// <summary>
/// Tests for <see cref="AttributeValueCodec"/> — the canonical, round-trippable
/// codec used wherever an attribute value is stored or transmitted. Scalars must
/// encode to their historical invariant-culture string; List attributes encode to
/// a JSON array and decode back to a typed <c>List&lt;T&gt;</c>.
/// </summary>
public class AttributeValueCodecTests
{
[Fact]
public void Encode_StringList_ProducesJsonArray() =>
Assert.Equal("[\"WO-1\",\"WO-2\"]",
AttributeValueCodec.Encode(new List<string> { "WO-1", "WO-2" }));
[Fact]
public void Encode_Scalar_String_ReturnedAsIs() =>
Assert.Equal("hello", AttributeValueCodec.Encode("hello"));
[Fact]
public void Encode_Scalar_Double_IsInvariant()
{
var original = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
Assert.Equal("1.5", AttributeValueCodec.Encode(1.5));
}
finally
{
CultureInfo.CurrentCulture = original;
}
}
[Fact]
public void Encode_Null_ReturnsNull() =>
Assert.Null(AttributeValueCodec.Encode(null));
[Fact]
public void Encode_EmptyList_IsBracketPair() =>
Assert.Equal("[]", AttributeValueCodec.Encode(new List<string>()));
[Fact]
public void Encode_StringWithComma_IsEscaped() =>
Assert.Equal("[\"ACME, Inc.\"]",
AttributeValueCodec.Encode(new List<string> { "ACME, Inc." }));
[Fact]
public void Encode_DoubleList_IsInvariant()
{
var original = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
Assert.Equal("[\"1.5\",\"2.5\"]",
AttributeValueCodec.Encode(new List<double> { 1.5, 2.5 }));
}
finally
{
CultureInfo.CurrentCulture = original;
}
}
[Fact]
public void RoundTrip_Int32List()
{
var json = AttributeValueCodec.Encode(new List<int> { 1, 2, 3 });
var back = (IList<int>)AttributeValueCodec.Decode(json, DataType.List, DataType.Int32)!;
Assert.Equal(new[] { 1, 2, 3 }, back);
}
[Fact]
public void RoundTrip_DoubleList_IsCultureInvariant()
{
var original = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
var json = AttributeValueCodec.Encode(new List<double> { 1.5, 2.5 });
var back = (IList<double>)AttributeValueCodec.Decode(json, DataType.List, DataType.Double)!;
Assert.Equal(new[] { 1.5, 2.5 }, back);
}
finally
{
CultureInfo.CurrentCulture = original;
}
}
[Fact]
public void RoundTrip_BoolList()
{
var json = AttributeValueCodec.Encode(new List<bool> { true, false, true });
var back = (IList<bool>)AttributeValueCodec.Decode(json, DataType.List, DataType.Boolean)!;
Assert.Equal(new[] { true, false, true }, back);
}
[Fact]
public void RoundTrip_DateTimeList_Iso8601()
{
var a = new DateTime(2026, 6, 15, 13, 45, 30, DateTimeKind.Utc);
var b = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var json = AttributeValueCodec.Encode(new List<DateTime> { a, b });
var back = (IList<DateTime>)AttributeValueCodec.Decode(json, DataType.List, DataType.DateTime)!;
Assert.Equal(new[] { a, b }, back);
}
[Fact]
public void Decode_StringArray_ProducesListOfString()
{
var back = (IList<string>)AttributeValueCodec.Decode("[\"a\",\"b\"]", DataType.List, DataType.String)!;
Assert.Equal(new[] { "a", "b" }, back);
}
[Fact]
public void Decode_Scalar_ReturnsString() =>
Assert.Equal("42", AttributeValueCodec.Decode("42", DataType.Int32, null));
[Fact]
public void Decode_List_NullValue_ReturnsNull() =>
Assert.Null(AttributeValueCodec.Decode(null, DataType.List, DataType.String));
[Fact]
public void Decode_List_EmptyValue_ReturnsNull() =>
Assert.Null(AttributeValueCodec.Decode("", DataType.List, DataType.String));
[Fact]
public void Decode_MalformedJson_Throws() =>
Assert.Throws<FormatException>(() =>
AttributeValueCodec.Decode("not json", DataType.List, DataType.String));
[Fact]
public void Decode_UnparseableElement_Throws() =>
Assert.Throws<FormatException>(() =>
AttributeValueCodec.Decode("[\"abc\"]", DataType.List, DataType.Int32));
[Fact]
public void Decode_NullElement_Throws() =>
Assert.Throws<FormatException>(() =>
AttributeValueCodec.Decode("[null]", DataType.List, DataType.String));
[Fact]
public void Decode_List_WithoutElementType_Throws() =>
Assert.Throws<FormatException>(() =>
AttributeValueCodec.Decode("[\"a\"]", DataType.List, null));
[Theory]
[InlineData(DataType.String, true)]
[InlineData(DataType.Int32, true)]
[InlineData(DataType.Float, true)]
[InlineData(DataType.Double, true)]
[InlineData(DataType.Boolean, true)]
[InlineData(DataType.DateTime, true)]
[InlineData(DataType.Binary, false)]
[InlineData(DataType.List, false)]
public void IsValidElementType_MatchesScalarSet(DataType t, bool expected) =>
Assert.Equal(expected, AttributeValueCodec.IsValidElementType(t));
}