From 80d4d3e2527483f0ac7051b10a57dab7fe538061 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 12 May 2026 00:52:55 -0400 Subject: [PATCH] feat(commons): OpcUaEndpointConfigValidator --- .../OpcUaEndpointConfigValidator.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/ScadaLink.Commons/Validators/OpcUaEndpointConfigValidator.cs diff --git a/src/ScadaLink.Commons/Validators/OpcUaEndpointConfigValidator.cs b/src/ScadaLink.Commons/Validators/OpcUaEndpointConfigValidator.cs new file mode 100644 index 0000000..d59777e --- /dev/null +++ b/src/ScadaLink.Commons/Validators/OpcUaEndpointConfigValidator.cs @@ -0,0 +1,62 @@ +using ScadaLink.Commons.Types.DataConnections; +using ScadaLink.Commons.Types.Flattening; + +namespace ScadaLink.Commons.Validators; + +/// +/// Pure-function validator for . Errors carry +/// the offending property name in +/// (optionally prefixed, e.g. "Primary.EndpointUrl") so the form can render +/// per-field messages. +/// +public static class OpcUaEndpointConfigValidator +{ + public static ValidationResult Validate(OpcUaEndpointConfig config, string fieldPrefix = "") + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(config.EndpointUrl)) + errors.Add(Err("EndpointUrl", "Endpoint URL is required.")); + else if (!Uri.TryCreate(config.EndpointUrl, UriKind.Absolute, out var uri) + || uri.Scheme != "opc.tcp" + || string.IsNullOrEmpty(uri.Host)) + errors.Add(Err("EndpointUrl", "Endpoint URL must be a valid opc.tcp:// URI.")); + + if (config.SessionTimeoutMs <= 0) + errors.Add(Err("SessionTimeoutMs", "Must be > 0.")); + if (config.OperationTimeoutMs <= 0) + errors.Add(Err("OperationTimeoutMs", "Must be > 0.")); + if (config.PublishingIntervalMs <= 0) + errors.Add(Err("PublishingIntervalMs", "Must be > 0.")); + if (config.SamplingIntervalMs <= 0) + errors.Add(Err("SamplingIntervalMs", "Must be > 0.")); + if (config.QueueSize < 1) + errors.Add(Err("QueueSize", "Must be ≥ 1.")); + if (config.KeepAliveCount < 1) + errors.Add(Err("KeepAliveCount", "Must be ≥ 1.")); + if (config.LifetimeCount < config.KeepAliveCount * 3) + errors.Add(Err("LifetimeCount", + "Must be at least 3× KeepAliveCount per OPC UA spec.")); + if (config.MaxNotificationsPerPublish < 1) + errors.Add(Err("MaxNotificationsPerPublish", "Must be ≥ 1.")); + + if (config.Heartbeat is { } hb) + { + if (string.IsNullOrWhiteSpace(hb.TagPath)) + errors.Add(Err("Heartbeat.TagPath", + "Tag path is required when heartbeat is enabled.")); + if (hb.MaxSilenceSeconds <= 0) + errors.Add(Err("Heartbeat.MaxSilenceSeconds", "Must be > 0.")); + } + + return errors.Count == 0 + ? ValidationResult.Success() + : ValidationResult.FromErrors(errors.ToArray()); + + ValidationEntry Err(string field, string message) => + ValidationEntry.Error( + ValidationCategory.ConnectionConfig, + message, + entityName: $"{fieldPrefix}{field}"); + } +}