diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Serialization/MxGatewayEndpointConfigSerializer.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Serialization/MxGatewayEndpointConfigSerializer.cs new file mode 100644 index 00000000..40282828 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Serialization/MxGatewayEndpointConfigSerializer.cs @@ -0,0 +1,65 @@ +using System.Globalization; +using System.Text.Json; +using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Serialization; + +/// +/// Serializes to/from the typed JSON stored in +/// DataConnection.PrimaryConfiguration / BackupConfiguration, and flattens +/// it to the IDictionary<string,string> shape IDataConnection.ConnectAsync +/// expects. MxGateway is net-new, so there is no legacy shape to recover — a row that +/// fails to parse yields a default config. +/// +public static class MxGatewayEndpointConfigSerializer +{ + private static readonly JsonSerializerOptions JsonOpts = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false, + }; + + /// Serializes a config to the typed JSON shape. + /// The endpoint configuration to serialize. + public static string Serialize(MxGatewayEndpointConfig config) + => JsonSerializer.Serialize(config, JsonOpts); + + /// Parses stored config JSON; null/blank/malformed yields a default config. + /// The stored JSON string. + public static MxGatewayEndpointConfig Deserialize(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return new MxGatewayEndpointConfig(); + try { return JsonSerializer.Deserialize(json, JsonOpts) ?? new MxGatewayEndpointConfig(); } + catch (JsonException) { return new MxGatewayEndpointConfig(); } + } + + /// Flattens the typed config to the key-value shape the adapter consumes. + /// The endpoint configuration to flatten. + public static IDictionary ToFlatDict(MxGatewayEndpointConfig c) => new Dictionary + { + ["Endpoint"] = c.Endpoint, + ["ApiKey"] = c.ApiKey, + ["ClientName"] = c.ClientName, + ["WriteUserId"] = c.WriteUserId.ToString(CultureInfo.InvariantCulture), + ["UseTls"] = c.UseTls.ToString(), + ["CaFile"] = c.CaFile, + ["ServerName"] = c.ServerName, + ["ReadTimeoutMs"] = c.ReadTimeoutMs.ToString(CultureInfo.InvariantCulture), + }; + + /// Reconstructs a config from the flat key-value shape; invalid numerics fall back to defaults. + /// The flat dictionary. + public static MxGatewayEndpointConfig FromFlatDict(IDictionary d) + { + var c = new MxGatewayEndpointConfig(); + if (d.TryGetValue("Endpoint", out var ep) && !string.IsNullOrWhiteSpace(ep)) c.Endpoint = ep; + if (d.TryGetValue("ApiKey", out var ak)) c.ApiKey = ak; + if (d.TryGetValue("ClientName", out var cn)) c.ClientName = cn; + if (d.TryGetValue("WriteUserId", out var wu) && int.TryParse(wu, out var wuv)) c.WriteUserId = wuv; + if (d.TryGetValue("UseTls", out var tls) && bool.TryParse(tls, out var tlsv)) c.UseTls = tlsv; + if (d.TryGetValue("CaFile", out var ca)) c.CaFile = ca; + if (d.TryGetValue("ServerName", out var sn)) c.ServerName = sn; + if (d.TryGetValue("ReadTimeoutMs", out var rt) && int.TryParse(rt, out var rtv)) c.ReadTimeoutMs = rtv; + return c; + } +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/DataConnections/MxGatewayEndpointConfigSerializerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/DataConnections/MxGatewayEndpointConfigSerializerTests.cs new file mode 100644 index 00000000..2493111c --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/DataConnections/MxGatewayEndpointConfigSerializerTests.cs @@ -0,0 +1,84 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Serialization; +using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections; + +public class MxGatewayEndpointConfigSerializerTests +{ + [Fact] + public void Serialize_then_Deserialize_round_trips_all_fields() + { + var original = new MxGatewayEndpointConfig + { + Endpoint = "https://gw:5001", + ApiKey = "secret-key", + ClientName = "client-a", + WriteUserId = 7, + UseTls = true, + CaFile = "/certs/ca.pem", + ServerName = "gw.local", + ReadTimeoutMs = 1234 + }; + + var json = MxGatewayEndpointConfigSerializer.Serialize(original); + var round = MxGatewayEndpointConfigSerializer.Deserialize(json); + + Assert.Equal(original.Endpoint, round.Endpoint); + Assert.Equal(original.ApiKey, round.ApiKey); + Assert.Equal(original.ClientName, round.ClientName); + Assert.Equal(original.WriteUserId, round.WriteUserId); + Assert.Equal(original.UseTls, round.UseTls); + Assert.Equal(original.CaFile, round.CaFile); + Assert.Equal(original.ServerName, round.ServerName); + Assert.Equal(original.ReadTimeoutMs, round.ReadTimeoutMs); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("{ not valid json")] + public void Deserialize_null_blank_or_malformed_returns_default(string? json) + { + var def = new MxGatewayEndpointConfig(); + var result = MxGatewayEndpointConfigSerializer.Deserialize(json); + Assert.Equal(def.Endpoint, result.Endpoint); + Assert.Equal(def.ReadTimeoutMs, result.ReadTimeoutMs); + } + + [Fact] + public void ToFlatDict_FromFlatDict_round_trips() + { + var original = new MxGatewayEndpointConfig + { + Endpoint = "http://x:5000", + ApiKey = "k", + ClientName = "c", + WriteUserId = 3, + UseTls = true, + CaFile = "/ca", + ServerName = "s", + ReadTimeoutMs = 999 + }; + + var dict = MxGatewayEndpointConfigSerializer.ToFlatDict(original); + var round = MxGatewayEndpointConfigSerializer.FromFlatDict(dict); + + Assert.Equal(original.Endpoint, round.Endpoint); + Assert.Equal(original.ApiKey, round.ApiKey); + Assert.Equal(original.ClientName, round.ClientName); + Assert.Equal(original.WriteUserId, round.WriteUserId); + Assert.Equal(original.UseTls, round.UseTls); + Assert.Equal(original.CaFile, round.CaFile); + Assert.Equal(original.ServerName, round.ServerName); + Assert.Equal(original.ReadTimeoutMs, round.ReadTimeoutMs); + } + + [Fact] + public void FromFlatDict_invalid_numeric_falls_back_to_default() + { + var back = MxGatewayEndpointConfigSerializer.FromFlatDict( + new Dictionary { ["ReadTimeoutMs"] = "not-a-number" }); + Assert.Equal(new MxGatewayEndpointConfig().ReadTimeoutMs, back.ReadTimeoutMs); + } +}