fix(commons): resolve Commons-005..007,009..012 — OPC UA parse status, TryConvert correctness, Result null guard, invariant formatting, doc refresh

This commit is contained in:
Joseph Doherty
2026-05-16 22:04:21 -04:00
parent 746ab90444
commit c07f524ca4
12 changed files with 602 additions and 99 deletions

View File

@@ -4,11 +4,73 @@ using ScadaLink.Commons.Types.DataConnections;
namespace ScadaLink.Commons.Serialization;
/// <summary>
/// Outcome classification for <see cref="OpcUaEndpointConfigSerializer.Deserialize"/>.
/// </summary>
public enum OpcUaConfigParseStatus
{
/// <summary>The stored JSON parsed cleanly as the current typed shape.</summary>
Typed,
/// <summary>
/// The stored JSON parsed as the legacy flat string-dict shape. The returned
/// config is usable; the caller may prompt the user to re-save in the new shape.
/// </summary>
Legacy,
/// <summary>
/// The stored JSON could not be parsed at all (genuinely malformed). The returned
/// config is an empty default and the original string was lost — the caller should
/// surface an error rather than presenting the empty config as the user's data.
/// </summary>
Malformed
}
/// <summary>
/// Result of <see cref="OpcUaEndpointConfigSerializer.Deserialize"/>. Carries the parsed
/// config plus an explicit <see cref="Status"/> distinguishing a recoverable legacy row
/// from genuinely unparseable input. Deconstructs into <c>(Config, IsLegacy)</c> for
/// backward compatibility with callers that only need those two values.
/// </summary>
public readonly record struct OpcUaConfigParseResult
{
public OpcUaConfigParseResult(OpcUaEndpointConfig config, OpcUaConfigParseStatus status)
{
Config = config;
Status = status;
}
/// <summary>The parsed config (an empty default when <see cref="Status"/> is Malformed).</summary>
public OpcUaEndpointConfig Config { get; }
/// <summary>Classification of the parse outcome.</summary>
public OpcUaConfigParseStatus Status { get; }
/// <summary>True when the source parsed as the legacy flat-dict shape.</summary>
public bool IsLegacy => Status == OpcUaConfigParseStatus.Legacy;
/// <summary>True when the source could not be parsed at all.</summary>
public bool IsMalformed => Status == OpcUaConfigParseStatus.Malformed;
/// <summary>
/// Two-element deconstruction kept for backward compatibility. Note that
/// <c>IsLegacy</c> is <c>false</c> for both <see cref="OpcUaConfigParseStatus.Typed"/>
/// and <see cref="OpcUaConfigParseStatus.Malformed"/>; callers that need to tell those
/// apart should read <see cref="Status"/> directly.
/// </summary>
public void Deconstruct(out OpcUaEndpointConfig config, out bool isLegacy)
{
config = Config;
isLegacy = IsLegacy;
}
}
/// <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 returns IsLegacy=true so the form can prompt the user to re-save.
/// and reports <see cref="OpcUaConfigParseStatus.Legacy"/> so the form can prompt the
/// user to re-save.
/// </summary>
public static class OpcUaEndpointConfigSerializer
{
@@ -22,10 +84,25 @@ public static class OpcUaEndpointConfigSerializer
public static string Serialize(OpcUaEndpointConfig config)
=> JsonSerializer.Serialize(config, JsonOpts);
public static (OpcUaEndpointConfig Config, bool IsLegacy) Deserialize(string? json)
/// <summary>
/// Parses stored OPC UA endpoint JSON. Tries the current typed shape first, then the
/// legacy flat string-dict shape. The returned <see cref="OpcUaConfigParseResult.Status"/>
/// distinguishes three outcomes:
/// <list type="bullet">
/// <item><see cref="OpcUaConfigParseStatus.Typed"/> — clean parse of the current shape
/// (also returned for null/blank input, which yields a default config).</item>
/// <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>
/// </list>
/// </summary>
public static OpcUaConfigParseResult Deserialize(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return (new OpcUaEndpointConfig(), false);
return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Typed);
try
{
@@ -35,18 +112,21 @@ public static class OpcUaEndpointConfigSerializer
{
var typed = JsonSerializer.Deserialize<OpcUaEndpointConfig>(json, JsonOpts);
if (typed != null)
return (typed, false);
return new OpcUaConfigParseResult(typed, OpcUaConfigParseStatus.Typed);
}
}
catch (JsonException) { /* fall through to legacy */ }
try
{
return (LoadLegacy(json!), IsLegacy: true);
return new OpcUaConfigParseResult(LoadLegacy(json!), OpcUaConfigParseStatus.Legacy);
}
catch (JsonException)
{
return (new OpcUaEndpointConfig(), IsLegacy: true);
// Genuinely malformed input: not a recoverable legacy row. Report Malformed
// (not Legacy) so the caller can surface an error instead of presenting an
// empty config as if it were the user's saved configuration.
return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Malformed);
}
}