feat(commons): OpcUaEndpointConfigSerializer with legacy fallback + flat-dict interop
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ScadaLink.Commons.Types.DataConnections;
|
||||
|
||||
namespace ScadaLink.Commons.Serialization;
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public static class OpcUaEndpointConfigSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
public static string Serialize(OpcUaEndpointConfig config)
|
||||
=> JsonSerializer.Serialize(config, JsonOpts);
|
||||
|
||||
public static (OpcUaEndpointConfig Config, bool IsLegacy) Deserialize(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return (new OpcUaEndpointConfig(), false);
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.Object
|
||||
&& doc.RootElement.TryGetProperty("endpointUrl", out _))
|
||||
{
|
||||
var typed = JsonSerializer.Deserialize<OpcUaEndpointConfig>(json, JsonOpts);
|
||||
if (typed != null)
|
||||
return (typed, false);
|
||||
}
|
||||
}
|
||||
catch (JsonException) { /* fall through to legacy */ }
|
||||
|
||||
return (LoadLegacy(json!), IsLegacy: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens the typed config to the IDictionary<string,string> shape that
|
||||
/// IDataConnection.ConnectAsync expects. Keys match the historical convention
|
||||
/// used by OpcUaDataConnection so the adapter can keep that interface.
|
||||
/// </summary>
|
||||
public static IDictionary<string, string> ToFlatDict(OpcUaEndpointConfig config)
|
||||
{
|
||||
var dict = new Dictionary<string, string>
|
||||
{
|
||||
["endpoint"] = config.EndpointUrl,
|
||||
["SecurityMode"] = config.SecurityMode.ToString(),
|
||||
["AutoAcceptUntrustedCerts"] = config.AutoAcceptUntrustedCerts.ToString(),
|
||||
["SessionTimeoutMs"] = config.SessionTimeoutMs.ToString(),
|
||||
["OperationTimeoutMs"] = config.OperationTimeoutMs.ToString(),
|
||||
["PublishingIntervalMs"] = config.PublishingIntervalMs.ToString(),
|
||||
["SamplingIntervalMs"] = config.SamplingIntervalMs.ToString(),
|
||||
["QueueSize"] = config.QueueSize.ToString(),
|
||||
["KeepAliveCount"] = config.KeepAliveCount.ToString(),
|
||||
["LifetimeCount"] = config.LifetimeCount.ToString(),
|
||||
["MaxNotificationsPerPublish"] = config.MaxNotificationsPerPublish.ToString(),
|
||||
};
|
||||
if (config.Heartbeat is { } hb)
|
||||
{
|
||||
dict["HeartbeatTagPath"] = hb.TagPath;
|
||||
dict["HeartbeatMaxSilence"] = hb.MaxSilenceSeconds.ToString();
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
if (dict.TryGetValue("HeartbeatTagPath", out var hbPath)
|
||||
&& !string.IsNullOrWhiteSpace(hbPath))
|
||||
{
|
||||
c.Heartbeat = new OpcUaHeartbeatConfig
|
||||
{
|
||||
TagPath = hbPath,
|
||||
MaxSilenceSeconds = ParseInt(dict, "HeartbeatMaxSilence", 30)
|
||||
};
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
private static OpcUaEndpointConfig LoadLegacy(string json)
|
||||
{
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json)
|
||||
?? new Dictionary<string, string>();
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user