using System.Text.Json; using System.Text.Json.Serialization; using ScadaLink.Commons.Types.DataConnections; namespace ScadaLink.Commons.Serialization; /// /// Serializes to/from the typed nested JSON /// shape stored in DataConnection.PrimaryConfiguration / BackupConfiguration. /// 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. /// 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(json, JsonOpts); if (typed != null) return (typed, false); } } catch (JsonException) { /* fall through to legacy */ } try { return (LoadLegacy(json!), IsLegacy: true); } catch (JsonException) { return (new OpcUaEndpointConfig(), IsLegacy: true); } } /// /// 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. /// public static IDictionary ToFlatDict(OpcUaEndpointConfig config) { var dict = new Dictionary { ["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 dict) { 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(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)) { var hb = new OpcUaHeartbeatConfig { TagPath = hbPath }; TryAssignInt(dict, "HeartbeatMaxSilence", v => hb.MaxSilenceSeconds = v); c.Heartbeat = hb; } return c; } private static OpcUaEndpointConfig LoadLegacy(string json) { var dict = JsonSerializer.Deserialize>(json) ?? new Dictionary(); return FromFlatDict(dict); } private static void TryAssignInt(IDictionary dict, string key, Action assign) { if (dict.TryGetValue(key, out var s) && int.TryParse(s, out var v)) assign(v); } }