feat(commons): Layer B serializer + validator handle new OPC UA settings
OpcUaEndpointConfigSerializer: - ToFlatDict emits new scalar keys (DiscardOldest, SubscriptionPriority, SubscriptionDisplayName, TimestampsToReturn). - ToFlatDict emits dotted sub-object keys (UserIdentity.TokenType / Username / Password / CertificatePath / CertificatePassword, Deadband.Type / Value) when those sub-objects are non-null. - FromFlatDict reads the same keys back; missing keys preserve POCO defaults. - Deadband.Value uses InvariantCulture for double parsing/formatting. OpcUaEndpointConfigValidator: - SubscriptionDisplayName required (non-empty). - UserIdentity.UsernamePassword requires Username. - UserIdentity.X509Certificate requires CertificatePath. - Deadband.Value must be > 0 when Deadband is set. - fieldPrefix propagates through sub-object error EntityNames. Drives the 11 previously-failing tests green; 51/51 in the suite now pass.
This commit is contained in:
@@ -70,12 +70,29 @@ public static class OpcUaEndpointConfigSerializer
|
|||||||
["KeepAliveCount"] = config.KeepAliveCount.ToString(),
|
["KeepAliveCount"] = config.KeepAliveCount.ToString(),
|
||||||
["LifetimeCount"] = config.LifetimeCount.ToString(),
|
["LifetimeCount"] = config.LifetimeCount.ToString(),
|
||||||
["MaxNotificationsPerPublish"] = config.MaxNotificationsPerPublish.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)
|
if (config.Heartbeat is { } hb)
|
||||||
{
|
{
|
||||||
dict["HeartbeatTagPath"] = hb.TagPath;
|
dict["HeartbeatTagPath"] = hb.TagPath;
|
||||||
dict["HeartbeatMaxSilence"] = hb.MaxSilenceSeconds.ToString();
|
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;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +122,16 @@ public static class OpcUaEndpointConfigSerializer
|
|||||||
TryAssignInt(dict, "LifetimeCount", v => c.LifetimeCount = v);
|
TryAssignInt(dict, "LifetimeCount", v => c.LifetimeCount = v);
|
||||||
TryAssignInt(dict, "MaxNotificationsPerPublish", v => c.MaxNotificationsPerPublish = 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)
|
if (dict.TryGetValue("HeartbeatTagPath", out var hbPath)
|
||||||
&& !string.IsNullOrWhiteSpace(hbPath))
|
&& !string.IsNullOrWhiteSpace(hbPath))
|
||||||
{
|
{
|
||||||
@@ -113,6 +140,28 @@ public static class OpcUaEndpointConfigSerializer
|
|||||||
c.Heartbeat = hb;
|
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;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ public static class OpcUaEndpointConfigValidator
|
|||||||
if (config.MaxNotificationsPerPublish < 1)
|
if (config.MaxNotificationsPerPublish < 1)
|
||||||
errors.Add(Err("MaxNotificationsPerPublish", "Must be ≥ 1."));
|
errors.Add(Err("MaxNotificationsPerPublish", "Must be ≥ 1."));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(config.SubscriptionDisplayName))
|
||||||
|
errors.Add(Err("SubscriptionDisplayName",
|
||||||
|
"Subscription display name is required."));
|
||||||
|
|
||||||
if (config.Heartbeat is { } hb)
|
if (config.Heartbeat is { } hb)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(hb.TagPath))
|
if (string.IsNullOrWhiteSpace(hb.TagPath))
|
||||||
@@ -49,6 +53,21 @@ public static class OpcUaEndpointConfigValidator
|
|||||||
errors.Add(Err("Heartbeat.MaxSilenceSeconds", "Must be > 0."));
|
errors.Add(Err("Heartbeat.MaxSilenceSeconds", "Must be > 0."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.UserIdentity is { } ui)
|
||||||
|
{
|
||||||
|
if (ui.TokenType == OpcUaUserTokenType.UsernamePassword
|
||||||
|
&& string.IsNullOrWhiteSpace(ui.Username))
|
||||||
|
errors.Add(Err("UserIdentity.Username",
|
||||||
|
"Username is required when token type is UsernamePassword."));
|
||||||
|
if (ui.TokenType == OpcUaUserTokenType.X509Certificate
|
||||||
|
&& string.IsNullOrWhiteSpace(ui.CertificatePath))
|
||||||
|
errors.Add(Err("UserIdentity.CertificatePath",
|
||||||
|
"Certificate path is required when token type is X509Certificate."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.Deadband is { } db && db.Value <= 0)
|
||||||
|
errors.Add(Err("Deadband.Value", "Must be > 0."));
|
||||||
|
|
||||||
return errors.Count == 0
|
return errors.Count == 0
|
||||||
? ValidationResult.Success()
|
? ValidationResult.Success()
|
||||||
: ValidationResult.FromErrors(errors.ToArray());
|
: ValidationResult.FromErrors(errors.ToArray());
|
||||||
|
|||||||
Reference in New Issue
Block a user