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

View File

@@ -114,6 +114,21 @@ public class OpcUaEndpointConfigSerializerTests
Assert.Equal("opc.tcp://x:4840", config.EndpointUrl); 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] [Fact]
public void ToFlatDict_OmitsNullHeartbeat() public void ToFlatDict_OmitsNullHeartbeat()
{ {