refactor(commons): defensive legacy-parse + FromFlatDict starts from POCO defaults

This commit is contained in:
Joseph Doherty
2026-05-12 00:48:17 -04:00
parent 8fbf167389
commit 4608adcd53
2 changed files with 56 additions and 29 deletions

View File

@@ -40,8 +40,15 @@ public static class OpcUaEndpointConfigSerializer
}
catch (JsonException) { /* fall through to legacy */ }
try
{
return (LoadLegacy(json!), IsLegacy: true);
}
catch (JsonException)
{
return (new OpcUaEndpointConfig(), IsLegacy: true);
}
}
/// <summary>
/// Flattens the typed config to the IDictionary&lt;string,string&gt; shape that
@@ -74,33 +81,38 @@ public static class OpcUaEndpointConfigSerializer
public static OpcUaEndpointConfig FromFlatDict(IDictionary<string, string> dict)
{
var c = new OpcUaEndpointConfig
{
EndpointUrl = dict.TryGetValue("endpoint", out var ep) ? ep
: dict.TryGetValue("EndpointUrl", out var ep2) ? ep2 : "",
SecurityMode = Enum.TryParse<OpcUaSecurityMode>(
dict.TryGetValue("SecurityMode", out var smStr) ? smStr : null,
ignoreCase: true, out var sm)
? sm : OpcUaSecurityMode.None,
AutoAcceptUntrustedCerts = ParseBool(dict, "AutoAcceptUntrustedCerts", true),
SessionTimeoutMs = ParseInt(dict, "SessionTimeoutMs", 60000),
OperationTimeoutMs = ParseInt(dict, "OperationTimeoutMs", 15000),
PublishingIntervalMs = ParseInt(dict, "PublishingIntervalMs", 1000),
SamplingIntervalMs = ParseInt(dict, "SamplingIntervalMs", 1000),
QueueSize = ParseInt(dict, "QueueSize", 10),
KeepAliveCount = ParseInt(dict, "KeepAliveCount", 10),
LifetimeCount = ParseInt(dict, "LifetimeCount", 30),
MaxNotificationsPerPublish = ParseInt(dict, "MaxNotificationsPerPublish", 100),
};
var c = new OpcUaEndpointConfig();
if (dict.TryGetValue("endpoint", out var ep) && !string.IsNullOrWhiteSpace(ep))
c.EndpointUrl = ep;
else if (dict.TryGetValue("EndpointUrl", out var ep2) && !string.IsNullOrWhiteSpace(ep2))
c.EndpointUrl = ep2;
if (dict.TryGetValue("SecurityMode", out var smStr)
&& Enum.TryParse<OpcUaSecurityMode>(smStr, ignoreCase: true, out var sm))
c.SecurityMode = sm;
if (dict.TryGetValue("AutoAcceptUntrustedCerts", out var aacStr)
&& bool.TryParse(aacStr, out var aac))
c.AutoAcceptUntrustedCerts = aac;
TryAssignInt(dict, "SessionTimeoutMs", v => c.SessionTimeoutMs = v);
TryAssignInt(dict, "OperationTimeoutMs", v => c.OperationTimeoutMs = v);
TryAssignInt(dict, "PublishingIntervalMs", v => c.PublishingIntervalMs = v);
TryAssignInt(dict, "SamplingIntervalMs", v => c.SamplingIntervalMs = v);
TryAssignInt(dict, "QueueSize", v => c.QueueSize = v);
TryAssignInt(dict, "KeepAliveCount", v => c.KeepAliveCount = v);
TryAssignInt(dict, "LifetimeCount", v => c.LifetimeCount = v);
TryAssignInt(dict, "MaxNotificationsPerPublish", v => c.MaxNotificationsPerPublish = v);
if (dict.TryGetValue("HeartbeatTagPath", out var hbPath)
&& !string.IsNullOrWhiteSpace(hbPath))
{
c.Heartbeat = new OpcUaHeartbeatConfig
{
TagPath = hbPath,
MaxSilenceSeconds = ParseInt(dict, "HeartbeatMaxSilence", 30)
};
var hb = new OpcUaHeartbeatConfig { TagPath = hbPath };
TryAssignInt(dict, "HeartbeatMaxSilence", v => hb.MaxSilenceSeconds = v);
c.Heartbeat = hb;
}
return c;
}
@@ -111,9 +123,9 @@ public static class OpcUaEndpointConfigSerializer
return FromFlatDict(dict);
}
private static int ParseInt(IDictionary<string, string> d, string key, int defaultValue)
=> d.TryGetValue(key, out var s) && int.TryParse(s, out var v) ? v : defaultValue;
private static bool ParseBool(IDictionary<string, string> d, string key, bool defaultValue)
=> d.TryGetValue(key, out var s) && bool.TryParse(s, out var v) ? v : defaultValue;
private static void TryAssignInt(IDictionary<string, string> dict, string key, Action<int> assign)
{
if (dict.TryGetValue(key, out var s) && int.TryParse(s, out var v))
assign(v);
}
}

View File

@@ -114,6 +114,21 @@ public class OpcUaEndpointConfigSerializerTests
Assert.Equal("opc.tcp://x:4840", config.EndpointUrl);
}
[Theory]
[InlineData("not json at all")]
[InlineData("[1,2,3]")]
[InlineData("{\"foo\":123}")]
[InlineData("\"just a string\"")]
public void Deserialize_Malformed_ReturnsDefaultsAsLegacy(string input)
{
var (config, isLegacy) = OpcUaEndpointConfigSerializer.Deserialize(input);
Assert.True(isLegacy);
Assert.Equal("", config.EndpointUrl);
Assert.Equal(60000, config.SessionTimeoutMs);
Assert.Null(config.Heartbeat);
}
[Fact]
public void ToFlatDict_OmitsNullHeartbeat()
{