fix(commons): resolve Commons-013,014 — integral JSON index handling, distinguish Malformed vs Legacy OPC UA config

This commit is contained in:
Joseph Doherty
2026-05-17 03:18:17 -04:00
parent 21856a4be7
commit a78c3bcb6f
5 changed files with 190 additions and 18 deletions

View File

@@ -68,9 +68,11 @@ public readonly record struct OpcUaConfigParseResult
/// <summary>
/// Serializes <see cref="OpcUaEndpointConfig"/> to/from the typed nested JSON
/// shape stored in <c>DataConnection.PrimaryConfiguration</c> / <c>BackupConfiguration</c>.
/// On read, falls back to the legacy flat string-dict shape for pre-refactor rows
/// and reports <see cref="OpcUaConfigParseStatus.Legacy"/> so the form can prompt the
/// user to re-save.
/// On read, falls back to the legacy flat string-dict shape only for rows that are not
/// the current typed shape (no <c>endpointUrl</c> property), reporting
/// <see cref="OpcUaConfigParseStatus.Legacy"/> so the form can prompt the user to
/// re-save. A row that <em>is</em> the typed shape but fails to deserialize is reported
/// <see cref="OpcUaConfigParseStatus.Malformed"/>, never <see cref="OpcUaConfigParseStatus.Legacy"/>.
/// </summary>
public static class OpcUaEndpointConfigSerializer
{
@@ -94,9 +96,13 @@ public static class OpcUaEndpointConfigSerializer
/// <item><see cref="OpcUaConfigParseStatus.Legacy"/> — parsed as a legacy flat object;
/// the config is usable and the caller may prompt a re-save.</item>
/// <item><see cref="OpcUaConfigParseStatus.Malformed"/> — the input is genuinely
/// unparseable JSON. The config is an empty default and the original string is lost;
/// the caller should surface an error rather than treating the empty config as the
/// user's saved data.</item>
/// unparseable JSON, <em>or</em> it is the current typed shape (it has an
/// <c>endpointUrl</c> property) but typed deserialization failed — e.g. an
/// enum-valued field holding an unrecognised string or a wrong-typed field. Such a
/// corrupt typed row is reported <see cref="OpcUaConfigParseStatus.Malformed"/>
/// rather than being mislabelled <see cref="OpcUaConfigParseStatus.Legacy"/>, so the
/// offending field is not silently dropped. The config is an empty default and the
/// caller should surface an error rather than treating it as the user's saved data.</item>
/// </list>
/// </summary>
public static OpcUaConfigParseResult Deserialize(string? json)
@@ -104,22 +110,42 @@ public static class OpcUaEndpointConfigSerializer
if (string.IsNullOrWhiteSpace(json))
return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Typed);
// First decide which shape the row is — without yet trying to materialize it.
// A root JSON object carrying "endpointUrl" IS the current typed shape; anything
// else (no endpointUrl) is treated as a candidate legacy flat-dict row.
bool isTypedShape;
try
{
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.ValueKind == JsonValueKind.Object
&& doc.RootElement.TryGetProperty("endpointUrl", out _))
isTypedShape = doc.RootElement.ValueKind == JsonValueKind.Object
&& doc.RootElement.TryGetProperty("endpointUrl", out _);
}
catch (JsonException)
{
// Could not even parse the document: genuinely malformed input.
return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Malformed);
}
if (isTypedShape)
{
// The row is the current typed shape. If typed deserialization fails the row
// is a corrupt current-shape row (e.g. an invalid enum or wrong-typed field) —
// it must NOT fall through to the legacy path and be mislabelled Legacy, which
// would silently drop the offending field. Report Malformed instead.
try
{
var typed = JsonSerializer.Deserialize<OpcUaEndpointConfig>(json, JsonOpts);
if (typed != null)
return new OpcUaConfigParseResult(typed, OpcUaConfigParseStatus.Typed);
}
catch (JsonException) { /* corrupt typed row — classified Malformed below */ }
return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Malformed);
}
catch (JsonException) { /* fall through to legacy */ }
try
{
return new OpcUaConfigParseResult(LoadLegacy(json!), OpcUaConfigParseStatus.Legacy);
return new OpcUaConfigParseResult(LoadLegacy(json), OpcUaConfigParseStatus.Legacy);
}
catch (JsonException)
{