refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Outcome classification for <see cref="OpcUaEndpointConfigSerializer.Deserialize"/>.
|
||||
/// </summary>
|
||||
public enum OpcUaConfigParseStatus
|
||||
{
|
||||
/// <summary>The stored JSON parsed cleanly as the current typed shape.</summary>
|
||||
Typed,
|
||||
|
||||
/// <summary>
|
||||
/// The stored JSON parsed as the legacy flat string-dict shape. The returned
|
||||
/// config is usable; the caller may prompt the user to re-save in the new shape.
|
||||
/// </summary>
|
||||
Legacy,
|
||||
|
||||
/// <summary>
|
||||
/// The stored JSON could not be parsed at all (genuinely malformed). The returned
|
||||
/// config is an empty default and the original string was lost — the caller should
|
||||
/// surface an error rather than presenting the empty config as the user's data.
|
||||
/// </summary>
|
||||
Malformed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of <see cref="OpcUaEndpointConfigSerializer.Deserialize"/>. Carries the parsed
|
||||
/// config plus an explicit <see cref="Status"/> distinguishing a recoverable legacy row
|
||||
/// from genuinely unparseable input. Deconstructs into <c>(Config, IsLegacy)</c> for
|
||||
/// backward compatibility with callers that only need those two values.
|
||||
/// </summary>
|
||||
public readonly record struct OpcUaConfigParseResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the result with the parsed config and its classification status.
|
||||
/// </summary>
|
||||
/// <param name="config">The parsed endpoint config (or an empty default on malformed input).</param>
|
||||
/// <param name="status">Classification of the parse outcome.</param>
|
||||
public OpcUaConfigParseResult(OpcUaEndpointConfig config, OpcUaConfigParseStatus status)
|
||||
{
|
||||
Config = config;
|
||||
Status = status;
|
||||
}
|
||||
|
||||
/// <summary>The parsed config (an empty default when <see cref="Status"/> is Malformed).</summary>
|
||||
public OpcUaEndpointConfig Config { get; }
|
||||
|
||||
/// <summary>Classification of the parse outcome.</summary>
|
||||
public OpcUaConfigParseStatus Status { get; }
|
||||
|
||||
/// <summary>True when the source parsed as the legacy flat-dict shape.</summary>
|
||||
public bool IsLegacy => Status == OpcUaConfigParseStatus.Legacy;
|
||||
|
||||
/// <summary>True when the source could not be parsed at all.</summary>
|
||||
public bool IsMalformed => Status == OpcUaConfigParseStatus.Malformed;
|
||||
|
||||
/// <summary>
|
||||
/// Two-element deconstruction kept for backward compatibility. Note that
|
||||
/// <c>IsLegacy</c> is <c>false</c> for both <see cref="OpcUaConfigParseStatus.Typed"/>
|
||||
/// and <see cref="OpcUaConfigParseStatus.Malformed"/>; callers that need to tell those
|
||||
/// apart should read <see cref="Status"/> directly.
|
||||
/// </summary>
|
||||
/// <param name="config">Receives the parsed endpoint config.</param>
|
||||
/// <param name="isLegacy">Receives true when the source was the legacy flat-dict shape.</param>
|
||||
public void Deconstruct(out OpcUaEndpointConfig config, out bool isLegacy)
|
||||
{
|
||||
config = Config;
|
||||
isLegacy = IsLegacy;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 only for rows that are not
|
||||
/// the current typed shape (no <c>endpointUrl</c> property), reporting
|
||||
/// <see cref="OpcUaConfigParseStatus.Legacy"/> so the form can prompt the user to
|
||||
/// re-save. A row that <em>is</em> the typed shape but fails to deserialize is reported
|
||||
/// <see cref="OpcUaConfigParseStatus.Malformed"/>, never <see cref="OpcUaConfigParseStatus.Legacy"/>.
|
||||
/// </summary>
|
||||
public static class OpcUaEndpointConfigSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Serializes an <see cref="OpcUaEndpointConfig"/> to the current typed JSON shape.
|
||||
/// </summary>
|
||||
/// <param name="config">The endpoint configuration to serialize.</param>
|
||||
/// <returns>A JSON string representing the configuration.</returns>
|
||||
public static string Serialize(OpcUaEndpointConfig config)
|
||||
=> JsonSerializer.Serialize(config, JsonOpts);
|
||||
|
||||
/// <summary>
|
||||
/// Parses stored OPC UA endpoint JSON. Tries the current typed shape first, then the
|
||||
/// legacy flat string-dict shape. The returned <see cref="OpcUaConfigParseResult.Status"/>
|
||||
/// distinguishes three outcomes:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="OpcUaConfigParseStatus.Typed"/> — clean parse of the current shape
|
||||
/// (also returned for null/blank input, which yields a default config).</item>
|
||||
/// <item><see cref="OpcUaConfigParseStatus.Legacy"/> — parsed as a legacy flat object;
|
||||
/// the config is usable and the caller may prompt a re-save.</item>
|
||||
/// <item><see cref="OpcUaConfigParseStatus.Malformed"/> — the input is genuinely
|
||||
/// unparseable JSON, <em>or</em> it is the current typed shape (it has an
|
||||
/// <c>endpointUrl</c> property) but typed deserialization failed — e.g. an
|
||||
/// enum-valued field holding an unrecognised string or a wrong-typed field. Such a
|
||||
/// corrupt typed row is reported <see cref="OpcUaConfigParseStatus.Malformed"/>
|
||||
/// rather than being mislabelled <see cref="OpcUaConfigParseStatus.Legacy"/>, so the
|
||||
/// offending field is not silently dropped. The config is an empty default and the
|
||||
/// caller should surface an error rather than treating it as the user's saved data.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="json">The stored JSON string to parse; null or blank yields a default typed result.</param>
|
||||
public static OpcUaConfigParseResult Deserialize(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Typed);
|
||||
|
||||
// First decide which shape the row is — without yet trying to materialize it.
|
||||
// A root JSON object carrying "endpointUrl" IS the current typed shape; anything
|
||||
// else (no endpointUrl) is treated as a candidate legacy flat-dict row.
|
||||
bool isTypedShape;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
isTypedShape = doc.RootElement.ValueKind == JsonValueKind.Object
|
||||
&& doc.RootElement.TryGetProperty("endpointUrl", out _);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Could not even parse the document: genuinely malformed input.
|
||||
return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Malformed);
|
||||
}
|
||||
|
||||
if (isTypedShape)
|
||||
{
|
||||
// The row is the current typed shape. If typed deserialization fails the row
|
||||
// is a corrupt current-shape row (e.g. an invalid enum or wrong-typed field) —
|
||||
// it must NOT fall through to the legacy path and be mislabelled Legacy, which
|
||||
// would silently drop the offending field. Report Malformed instead.
|
||||
try
|
||||
{
|
||||
var typed = JsonSerializer.Deserialize<OpcUaEndpointConfig>(json, JsonOpts);
|
||||
if (typed != null)
|
||||
return new OpcUaConfigParseResult(typed, OpcUaConfigParseStatus.Typed);
|
||||
}
|
||||
catch (JsonException) { /* corrupt typed row — classified Malformed below */ }
|
||||
|
||||
return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Malformed);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new OpcUaConfigParseResult(LoadLegacy(json), OpcUaConfigParseStatus.Legacy);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Genuinely malformed input: not a recoverable legacy row. Report Malformed
|
||||
// (not Legacy) so the caller can surface an error instead of presenting an
|
||||
// empty config as if it were the user's saved configuration.
|
||||
return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Malformed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="config">The endpoint configuration to flatten.</param>
|
||||
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(),
|
||||
["DiscardOldest"] = config.DiscardOldest.ToString(),
|
||||
["SubscriptionPriority"] = config.SubscriptionPriority.ToString(),
|
||||
["SubscriptionDisplayName"] = config.SubscriptionDisplayName,
|
||||
["TimestampsToReturn"] = config.TimestampsToReturn.ToString(),
|
||||
};
|
||||
if (config.Heartbeat is { } hb)
|
||||
{
|
||||
dict["HeartbeatTagPath"] = hb.TagPath;
|
||||
dict["HeartbeatMaxSilence"] = hb.MaxSilenceSeconds.ToString();
|
||||
}
|
||||
if (config.UserIdentity is { } ui)
|
||||
{
|
||||
dict["UserIdentity.TokenType"] = ui.TokenType.ToString();
|
||||
dict["UserIdentity.Username"] = ui.Username;
|
||||
dict["UserIdentity.Password"] = ui.Password;
|
||||
dict["UserIdentity.CertificatePath"] = ui.CertificatePath;
|
||||
dict["UserIdentity.CertificatePassword"] = ui.CertificatePassword;
|
||||
}
|
||||
if (config.Deadband is { } db)
|
||||
{
|
||||
dict["Deadband.Type"] = db.Type.ToString();
|
||||
dict["Deadband.Value"] = db.Value.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs an <see cref="OpcUaEndpointConfig"/> from the legacy flat string-dict shape.
|
||||
/// </summary>
|
||||
/// <param name="dict">The flat key-value dictionary produced by the legacy shape.</param>
|
||||
/// <returns>The reconstructed endpoint configuration.</returns>
|
||||
public static OpcUaEndpointConfig FromFlatDict(IDictionary<string, string> 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<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("DiscardOldest", out var doStr) && bool.TryParse(doStr, out var doVal))
|
||||
c.DiscardOldest = doVal;
|
||||
if (dict.TryGetValue("SubscriptionPriority", out var spStr) && byte.TryParse(spStr, out var spVal))
|
||||
c.SubscriptionPriority = spVal;
|
||||
if (dict.TryGetValue("SubscriptionDisplayName", out var sdnStr) && !string.IsNullOrWhiteSpace(sdnStr))
|
||||
c.SubscriptionDisplayName = sdnStr;
|
||||
if (dict.TryGetValue("TimestampsToReturn", out var ttrStr)
|
||||
&& Enum.TryParse<OpcUaTimestampsToReturn>(ttrStr, ignoreCase: true, out var ttr))
|
||||
c.TimestampsToReturn = ttr;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (dict.TryGetValue("UserIdentity.TokenType", out var uiTt)
|
||||
&& Enum.TryParse<OpcUaUserTokenType>(uiTt, ignoreCase: true, out var tokenType))
|
||||
{
|
||||
var ui = new OpcUaUserIdentityConfig { TokenType = tokenType };
|
||||
if (dict.TryGetValue("UserIdentity.Username", out var u)) ui.Username = u;
|
||||
if (dict.TryGetValue("UserIdentity.Password", out var p)) ui.Password = p;
|
||||
if (dict.TryGetValue("UserIdentity.CertificatePath", out var cp)) ui.CertificatePath = cp;
|
||||
if (dict.TryGetValue("UserIdentity.CertificatePassword", out var cpw)) ui.CertificatePassword = cpw;
|
||||
c.UserIdentity = ui;
|
||||
}
|
||||
|
||||
if (dict.TryGetValue("Deadband.Type", out var dbT)
|
||||
&& Enum.TryParse<OpcUaDeadbandType>(dbT, ignoreCase: true, out var dbType))
|
||||
{
|
||||
var db = new OpcUaDeadbandConfig { Type = dbType };
|
||||
if (dict.TryGetValue("Deadband.Value", out var dbV)
|
||||
&& double.TryParse(dbV, System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var dbVal))
|
||||
db.Value = dbVal;
|
||||
c.Deadband = db;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
private static OpcUaEndpointConfig LoadLegacy(string json)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
||||
throw new JsonException("Legacy JSON must be a flat object.");
|
||||
|
||||
var dict = new Dictionary<string, string>();
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
// JsonElement.ToString() returns the raw value for primitives:
|
||||
// "1000" for numbers, "true"/"false" for booleans, the string content
|
||||
// for strings. Nested objects/arrays serialize to their JSON; we don't
|
||||
// expect those in the legacy flat-dict shape, but FromFlatDict will
|
||||
// simply ignore unknown keys.
|
||||
dict[prop.Name] = prop.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => prop.Value.GetString() ?? "",
|
||||
JsonValueKind.Number => prop.Value.GetRawText(),
|
||||
JsonValueKind.True => "True",
|
||||
JsonValueKind.False => "False",
|
||||
_ => prop.Value.ToString()
|
||||
};
|
||||
}
|
||||
return FromFlatDict(dict);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user