feat(dcl): add OpcUaReferenceForm.IsDurable — flags bare ns= bindings (v3 cutover #14)
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure, protocol-package-free classification of an OPC UA node-reference string's
|
||||||
|
/// <i>form</i> — used by authoring UI to nudge operators toward index-durable bindings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A bare <c>ns=<index>;…</c> reference hard-codes a namespace <i>index</i>, which is
|
||||||
|
/// only meaningful relative to one server's namespace table. The OtOpcUa v3.0 raw/uns
|
||||||
|
/// split makes this actively dangerous: v2's sole custom namespace and v3's <c>raw</c>
|
||||||
|
/// tree both land at <c>ns=2</c>, so a stored <c>ns=2;s=…</c> resolves without error but
|
||||||
|
/// silently now means a raw-tree node. The durable form is <c>nsu=<uri>;…</c>,
|
||||||
|
/// resolved against the live namespace table at use time by
|
||||||
|
/// <c>OpcUaNodeReference.Resolve</c>. This helper only classifies the string; it does not
|
||||||
|
/// parse or resolve NodeIds (that stays in the DataConnectionLayer seam, which owns the
|
||||||
|
/// <c>Opc.Ua</c> dependency).
|
||||||
|
/// </remarks>
|
||||||
|
public static class OpcUaReferenceForm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True when <paramref name="reference"/> is index-durable — the durable
|
||||||
|
/// <c>nsu=<uri>;…</c> form, spec-fixed <c>ns=0</c>, a short form with no namespace
|
||||||
|
/// prefix, or empty/whitespace (nothing to warn about). False only for a bare
|
||||||
|
/// server-namespace index (<c>ns=<n>;…</c> with n ≥ 1).
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsDurable(string? reference)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(reference))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var trimmed = reference.Trim();
|
||||||
|
|
||||||
|
// nsu=<uri>;… is the durable form (and its "ns" prefix must be checked before the
|
||||||
|
// bare "ns=" branch below, since "nsu=" also starts with "ns").
|
||||||
|
if (trimmed.StartsWith("nsu=", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Not a bare namespace-index reference at all → short forms (i=85, s=Foo) that
|
||||||
|
// imply namespace 0, which is spec-fixed and already durable.
|
||||||
|
if (!trimmed.StartsWith("ns=", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// ns=<digits>;… — parse the index; ns=0 is spec-fixed (durable), n ≥ 1 is bare.
|
||||||
|
var semicolon = trimmed.IndexOf(';');
|
||||||
|
var digits = semicolon >= 0 ? trimmed[3..semicolon] : trimmed[3..];
|
||||||
|
if (int.TryParse(digits, NumberStyles.None, CultureInfo.InvariantCulture, out var index))
|
||||||
|
return index == 0;
|
||||||
|
|
||||||
|
// Malformed "ns=" with non-numeric index — not a recognisable bare index; leave
|
||||||
|
// the real parse/reject to OpcUaNodeReference.Resolve. Don't warn on it here.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.DataConnections;
|
||||||
|
|
||||||
|
public class OpcUaReferenceFormTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
// durable → true
|
||||||
|
[InlineData("nsu=https://zb.com/otopcua/raw;s=Line1.Pump.Speed", true)]
|
||||||
|
[InlineData("nsu=https://zb.com/otopcua/uns;s=AreaA/Line1/Pump/Speed", true)]
|
||||||
|
[InlineData("NSU=https://x;s=y", true)] // case-insensitive prefix
|
||||||
|
[InlineData("ns=0;i=85", true)] // spec-fixed namespace 0
|
||||||
|
[InlineData("i=85", true)] // short form, implicit ns0
|
||||||
|
[InlineData("s=Devices.Pump1", true)] // no namespace prefix
|
||||||
|
[InlineData("", true)] // nothing typed
|
||||||
|
[InlineData(" ", true)]
|
||||||
|
[InlineData(null, true)]
|
||||||
|
// bare server-namespace index → false (warn)
|
||||||
|
[InlineData("ns=2;s=MyDevice.Temperature", false)]
|
||||||
|
[InlineData("ns=1;i=1001", false)]
|
||||||
|
[InlineData("ns=10;s=x", false)]
|
||||||
|
[InlineData(" ns=3;s=x ", false)] // trimmed before inspection
|
||||||
|
public void IsDurable_classifies_reference_forms(string? reference, bool expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, OpcUaReferenceForm.IsDurable(reference));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user