feat(core-abstractions): TagConfigJson shared strict-capable field readers (R2-11, 05/CONV-2)
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
{ "id": "T8", "subject": "DraftValidator swap: Configuration→Commons ProjectReference; ExtractTagConfigFullName → TagConfigIntent.ExplicitFullName (null-on-absent preserved)", "status": "completed", "blockedBy": ["T2", "T3"] },
|
||||
{ "id": "T9", "subject": "EquipmentNodeWalker.ExtractFullName delegates to TagConfigIntent; repoint tree-wide canonical-parser doc cites", "status": "completed", "blockedBy": ["T2", "T3"] },
|
||||
{ "id": "T10", "subject": "Retire OpcUaServer.Tests ExtractTag*Tests (authority moved to Commons.Tests); verify zero 'MUST parse identically' hits", "status": "completed", "blockedBy": ["T3", "T5"] },
|
||||
{ "id": "T11", "subject": "TagConfigJson strict-capable readers in Core.Abstractions (TryReadEnum Absent/Valid/Invalid, ReadEnumOrDefault, ReadWritable, DescribeInvalidEnum) (TDD)", "status": "pending", "blockedBy": [] },
|
||||
{ "id": "T11", "subject": "TagConfigJson strict-capable readers in Core.Abstractions (TryReadEnum Absent/Valid/Invalid, ReadEnumOrDefault, ReadWritable, DescribeInvalidEnum) (TDD)", "status": "completed", "blockedBy": [] },
|
||||
{ "id": "T12", "subject": "Modbus equipment-tag parser: freeze test, shared readers, honour writable key, Inspect warnings", "status": "pending", "blockedBy": ["T11"] },
|
||||
{ "id": "T13", "subject": "S7 equipment-tag parser: freeze test, shared readers, honour writable key, Inspect warnings (+ amend Contracts banner)", "status": "pending", "blockedBy": ["T11"] },
|
||||
{ "id": "T14", "subject": "AbCip equipment-tag parser: freeze test, shared readers (writable already honoured), Inspect warnings", "status": "pending", "blockedBy": ["T11"] },
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
/// <summary>The outcome of reading an enum-valued field from a TagConfig JSON object.</summary>
|
||||
public enum JsonEnumRead
|
||||
{
|
||||
/// <summary>The property is absent, or present but not a JSON string (nothing to validate).</summary>
|
||||
Absent,
|
||||
|
||||
/// <summary>The property is a JSON string that parses (case-insensitively) to the enum.</summary>
|
||||
Valid,
|
||||
|
||||
/// <summary>The property is a JSON string that does NOT parse to the enum (a typo'd value).</summary>
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared TagConfig JSON field readers for the six equipment-tag parsers (05/CONV-2). Replaces the
|
||||
/// six byte-identical private <c>ReadEnum</c> copies with one strict-capable home beside
|
||||
/// <see cref="EquipmentTagRefResolver{TDef}"/>.
|
||||
/// <para><b>Unknown-key handling is a deliberate non-goal.</b> The TagConfig blob is a SHARED
|
||||
/// namespace — platform intent keys (<c>FullName</c>, <c>alarm</c>, <c>isHistorized</c>, …) coexist
|
||||
/// with per-driver address keys (<c>region</c>, <c>dataType</c>, …) and forward-compat keys the typed
|
||||
/// editors preserve. There is no single canonical schema, so these readers read the known fields and
|
||||
/// ignore the rest; they NEVER warn on unknown keys.</para>
|
||||
/// </summary>
|
||||
public static class TagConfigJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads an enum-valued string field, distinguishing absent / valid / present-but-invalid so callers
|
||||
/// can be lenient (fall back) while still <em>reporting</em> the invalid case. A property that is
|
||||
/// absent or present-but-non-string yields <see cref="JsonEnumRead.Absent"/> (there is no enum
|
||||
/// string to validate); a present JSON string that parses case-insensitively yields
|
||||
/// <see cref="JsonEnumRead.Valid"/>; a present JSON string that does not parse yields
|
||||
/// <see cref="JsonEnumRead.Invalid"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enum type to parse.</typeparam>
|
||||
/// <param name="o">The TagConfig root object.</param>
|
||||
/// <param name="name">The property name to read.</param>
|
||||
/// <param name="value">The parsed enum on <see cref="JsonEnumRead.Valid"/>; otherwise <c>default</c>.</param>
|
||||
/// <returns>The read outcome.</returns>
|
||||
public static JsonEnumRead TryReadEnum<TEnum>(JsonElement o, string name, out TEnum value)
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
if (!o.TryGetProperty(name, out var e) || e.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
value = default;
|
||||
return JsonEnumRead.Absent;
|
||||
}
|
||||
if (Enum.TryParse(e.GetString(), ignoreCase: true, out value))
|
||||
return JsonEnumRead.Valid;
|
||||
value = default;
|
||||
return JsonEnumRead.Invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lenient enum read with today's exact semantics: absent OR invalid ⇒ <paramref name="fallback"/>;
|
||||
/// only a present, parseable string yields the parsed value. Bit-for-bit equivalent to the six
|
||||
/// retired private <c>ReadEnum</c> copies.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enum type to parse.</typeparam>
|
||||
/// <param name="o">The TagConfig root object.</param>
|
||||
/// <param name="name">The property name to read.</param>
|
||||
/// <param name="fallback">The value returned when the field is absent or invalid.</param>
|
||||
/// <returns>The parsed enum, or <paramref name="fallback"/>.</returns>
|
||||
public static TEnum ReadEnumOrDefault<TEnum>(JsonElement o, string name, TEnum fallback)
|
||||
where TEnum : struct, Enum
|
||||
=> TryReadEnum<TEnum>(o, name, out var v) == JsonEnumRead.Valid ? v : fallback;
|
||||
|
||||
/// <summary>
|
||||
/// Reads the optional <c>"writable"</c> flag with AbCip semantics (the model): an explicit JSON
|
||||
/// <c>false</c> ⇒ <see langword="false"/>; anything else (absent, <c>true</c>, or a non-bool token)
|
||||
/// ⇒ <paramref name="defaultValue"/>.
|
||||
/// </summary>
|
||||
/// <param name="o">The TagConfig root object.</param>
|
||||
/// <param name="defaultValue">The value returned unless the field is an explicit JSON <c>false</c>.</param>
|
||||
/// <returns><see langword="false"/> only for an explicit <c>false</c>; otherwise <paramref name="defaultValue"/>.</returns>
|
||||
public static bool ReadWritable(JsonElement o, bool defaultValue = true)
|
||||
=> o.TryGetProperty("writable", out var e) && e.ValueKind == JsonValueKind.False
|
||||
? false
|
||||
: defaultValue;
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable warning for a present-but-invalid enum field (naming the field, the offending
|
||||
/// value, and the valid enum members), or <see langword="null"/> when the field is absent or valid.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enum type expected.</typeparam>
|
||||
/// <param name="o">The TagConfig root object.</param>
|
||||
/// <param name="name">The property name to describe.</param>
|
||||
/// <returns>The warning text, or <see langword="null"/>.</returns>
|
||||
public static string? DescribeInvalidEnum<TEnum>(JsonElement o, string name)
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
if (TryReadEnum<TEnum>(o, name, out _) != JsonEnumRead.Invalid) return null;
|
||||
var raw = o.TryGetProperty(name, out var e) ? e.GetString() : null;
|
||||
var valid = string.Join(", ", Enum.GetNames<TEnum>());
|
||||
return $"value '{raw}' for '{name}' is not a valid {typeof(TEnum).Name}; valid: {valid}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Text.Json;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the shared strict-capable TagConfig field readers (R2-11, 05/CONV-2): the
|
||||
/// <see cref="TagConfigJson.TryReadEnum{TEnum}"/> Absent/Valid/Invalid matrix (case-insensitive),
|
||||
/// <see cref="TagConfigJson.ReadEnumOrDefault{TEnum}"/> lenient equivalence to the retired copies,
|
||||
/// <see cref="TagConfigJson.ReadWritable"/> explicit-false-only semantics, and
|
||||
/// <see cref="TagConfigJson.DescribeInvalidEnum{TEnum}"/> messages.
|
||||
/// </summary>
|
||||
public sealed class TagConfigJsonTests
|
||||
{
|
||||
/// <summary>Sample enum for the reader matrix (public so it can be an <c>[InlineData]</c> arg).</summary>
|
||||
public enum Sample { Alpha, Beta, Gamma }
|
||||
|
||||
private static JsonElement Root(string json) => JsonDocument.Parse(json).RootElement;
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"k\":\"Beta\"}", JsonEnumRead.Valid, Sample.Beta)]
|
||||
[InlineData("{\"k\":\"beta\"}", JsonEnumRead.Valid, Sample.Beta)] // case-insensitive
|
||||
[InlineData("{\"k\":\"GAMMA\"}", JsonEnumRead.Valid, Sample.Gamma)]
|
||||
[InlineData("{\"k\":\"Deltaa\"}", JsonEnumRead.Invalid, default(Sample))] // typo → Invalid
|
||||
[InlineData("{}", JsonEnumRead.Absent, default(Sample))] // absent
|
||||
[InlineData("{\"k\":123}", JsonEnumRead.Absent, default(Sample))] // non-string → Absent (lenient)
|
||||
[InlineData("{\"k\":null}", JsonEnumRead.Absent, default(Sample))]
|
||||
public void TryReadEnum_matrix(string json, JsonEnumRead expected, Sample expectedValue)
|
||||
{
|
||||
var outcome = TagConfigJson.TryReadEnum<Sample>(Root(json), "k", out var v);
|
||||
outcome.ShouldBe(expected);
|
||||
v.ShouldBe(expectedValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"k\":\"Gamma\"}", Sample.Gamma)] // valid → parsed
|
||||
[InlineData("{\"k\":\"typo\"}", Sample.Alpha)] // invalid → fallback
|
||||
[InlineData("{}", Sample.Alpha)] // absent → fallback
|
||||
[InlineData("{\"k\":42}", Sample.Alpha)] // non-string → fallback
|
||||
public void ReadEnumOrDefault_is_lenient(string json, Sample expected)
|
||||
{
|
||||
TagConfigJson.ReadEnumOrDefault(Root(json), "k", Sample.Alpha).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"writable\":false}", true, false)] // explicit false → false
|
||||
[InlineData("{\"writable\":true}", true, true)] // explicit true → default
|
||||
[InlineData("{}", true, true)] // absent → default
|
||||
[InlineData("{\"writable\":\"no\"}", true, true)] // non-bool → default
|
||||
[InlineData("{}", false, false)] // absent, default false
|
||||
[InlineData("{\"writable\":false}", false, false)] // explicit false, default false
|
||||
public void ReadWritable_explicit_false_only(string json, bool dflt, bool expected)
|
||||
{
|
||||
TagConfigJson.ReadWritable(Root(json), dflt).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DescribeInvalidEnum_names_field_value_and_valid_members()
|
||||
{
|
||||
var msg = TagConfigJson.DescribeInvalidEnum<Sample>(Root("{\"dataType\":\"Betaa\"}"), "dataType");
|
||||
msg.ShouldNotBeNull();
|
||||
msg.ShouldContain("Betaa");
|
||||
msg.ShouldContain("dataType");
|
||||
msg.ShouldContain("Alpha");
|
||||
msg.ShouldContain("Beta");
|
||||
msg.ShouldContain("Gamma");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"dataType\":\"Beta\"}")] // valid → null
|
||||
[InlineData("{}")] // absent → null
|
||||
[InlineData("{\"dataType\":5}")] // non-string → null
|
||||
public void DescribeInvalidEnum_null_when_absent_or_valid(string json)
|
||||
{
|
||||
TagConfigJson.DescribeInvalidEnum<Sample>(Root(json), "dataType").ShouldBeNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user