fix(adminui): authoring must not accept a TagConfig enum the runtime rejects (#549)
MTConnectTagConfigModel read `dataType` with TagConfigJson.GetEnum, which falls
back to String on ANY unrecognised value. A misspelled name ("Strng") from a
hand-authored blob, a CSV import or the API was therefore accepted at authoring
and silently rewritten, while the driver's strict parse rejected it and the tag
died with BadNodeIdUnknown. Found by the G-7 parity tests' falsifiability
control, which had to use the digits-only shape to have a case that failed.
The rule is not restated in the AdminUI. MTConnectTagDefinitionFactory exposes
its parse as `IsAcceptedDataTypeName`, documented as the one authority the
driver, the deploy gate and the authoring model all consult; the model's
Validate() asks it about the raw value the blob carries. The old digits-only
message is NOT retained as a special case: the runtime draws no distinction
between a bare number and a typo — one predicate, one Inspect warning — so a
second message here would be a second statement of a rule this model does not
own. The raw value is captured as the JSON TOKEN, because the runtime
deserialises the key into a `string?` DTO field and an unquoted number throws
there too.
The survey found the same asymmetry at six more editors, contrary to the
assumption that those runtimes silently default: R2-11 Phase C moved every
driver's TagConfig enum read to TagConfigJson.TryReadEnumStrict, which REJECTS
the tag on a present-but-invalid string, while the editors kept loading them
with a silent fallback. Modbus (region/dataType/byteOrder), S7, AbCip, AbLegacy,
TwinCAT and FOCAS now check their enums in Validate() through the runtime's own
TagConfigJson.DescribeInvalidEnumText — added alongside a public
ClassifyEnumText that TryReadEnum now delegates to, so the JSON-token door and
the string door reach one implementation. Mqtt and Sql already carried
equivalent strict checks and are untouched.
Care taken NOT to become stricter than the runtime: a present-but-non-string
token stays accepted for those six (TryReadEnum calls it absent), as does an
absent key, each pinned by a test.
Also adds a direct ProjectReference to Core.Abstractions from the AdminUI — the
save gate now leans on a type it was consuming only transitively.
This commit is contained in:
@@ -48,7 +48,39 @@ public static class TagConfigJson
|
||||
value = default;
|
||||
return JsonEnumRead.Absent;
|
||||
}
|
||||
if (Enum.TryParse(e.GetString(), ignoreCase: true, out value))
|
||||
return ClassifyEnumText(e.GetString(), out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text-level heart of <see cref="TryReadEnum{TEnum}"/>, and with it the single authority on
|
||||
/// which enum spellings a TagConfig may carry: classifies an already-extracted enum STRING.
|
||||
/// <para>
|
||||
/// It is public because the authoring side reads the same blob through a different door — the
|
||||
/// AdminUI's typed tag-config editors hold a mutable <c>JsonObject</c> key bag, not a
|
||||
/// <see cref="JsonElement"/> — and an authoring gate that RESTATED the accept/reject boundary
|
||||
/// would drift from the runtime the moment either side moved, in the one direction that hurts:
|
||||
/// a value the modal accepts and the driver then rejects (Gitea #549). Both doors reach this
|
||||
/// method.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see langword="null"/> is <see cref="JsonEnumRead.Absent"/>, matching the JSON reader's
|
||||
/// treatment of an absent or present-but-non-string property — so a caller that extracts "the
|
||||
/// string value, or null" gets the runtime's leniency for free.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enum type to parse.</typeparam>
|
||||
/// <param name="text">The authored value's text, or <see langword="null"/> when there is none.</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 ClassifyEnumText<TEnum>(string? text, out TEnum value)
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
value = default;
|
||||
return JsonEnumRead.Absent;
|
||||
}
|
||||
if (Enum.TryParse(text, ignoreCase: true, out value))
|
||||
return JsonEnumRead.Valid;
|
||||
value = default;
|
||||
return JsonEnumRead.Invalid;
|
||||
@@ -120,10 +152,26 @@ public static class TagConfigJson
|
||||
/// <returns>The warning text, or <see langword="null"/>.</returns>
|
||||
public static string? DescribeInvalidEnum<TEnum>(JsonElement o, string name)
|
||||
where TEnum : struct, Enum
|
||||
=> TryReadEnum<TEnum>(o, name, out _) != JsonEnumRead.Invalid
|
||||
? null
|
||||
: DescribeInvalidEnumText<TEnum>(name, o.TryGetProperty(name, out var e) ? e.GetString() : null);
|
||||
|
||||
/// <summary>
|
||||
/// The text-level counterpart of <see cref="DescribeInvalidEnum{TEnum}(JsonElement, string)"/>,
|
||||
/// over the same <see cref="ClassifyEnumText{TEnum}"/> verdict: a human-readable warning for a
|
||||
/// present-but-invalid enum value, or <see langword="null"/> when it is absent
|
||||
/// (<paramref name="text"/> <see langword="null"/>) or valid. The authoring gate uses this so its
|
||||
/// wording, and not only its verdict, comes from the same place as the deploy gate's.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enum type expected.</typeparam>
|
||||
/// <param name="name">The property name to describe.</param>
|
||||
/// <param name="text">The authored value's text, or <see langword="null"/> when there is none.</param>
|
||||
/// <returns>The warning text, or <see langword="null"/>.</returns>
|
||||
public static string? DescribeInvalidEnumText<TEnum>(string name, string? text)
|
||||
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;
|
||||
if (ClassifyEnumText<TEnum>(text, out _) != JsonEnumRead.Invalid) return null;
|
||||
var valid = string.Join(", ", Enum.GetNames<TEnum>());
|
||||
return $"value '{raw}' for '{name}' is not a valid {typeof(TEnum).Name}; valid: {valid}";
|
||||
return $"value '{text}' for '{name}' is not a valid {typeof(TEnum).Name}; valid: {valid}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user