feat(v3-batch4): contracts — dual-namespace URIs, V3NodeIds, AddressSpaceRealm
Batch 4 contracts commit (coordinator lands before Wave A fan-out). Purely additive so wave agents branch from a green base: - AddressSpaceRealm enum (Raw | Uns) — the explicit realm discriminator that travels alongside a node's s= identifier at every sink seam (WP2 adds it to IOpcUaAddressSpaceSink / ISurgicalAddressSpaceSink; WP3 threads it). - V3NodeIds — the two namespace URI constants (https://zb.com/otopcua/raw, https://zb.com/otopcua/uns, replacing the single .../ns) + Raw() pass-through (s= id == RawPath) + Uns()/UnsChild() slash-joined Area/Line/Equipment[/Eff] builders, both sharing RawPaths.Separator + ordinal segment validation. Placement note: URIs live in V3NodeIds (Commons) rather than beside the retired OtOpcUaNodeManager.DefaultNamespaceUri so runtime + tests reference them without depending on OpcUaServer (single-source-of-truth, §5). WP2 rewires the node manager to register both via V3NodeIds.RawNamespaceUri/UnsNamespaceUri. EquipmentNodeIds retirement is NOT in this commit: its callers are the applier + runtime (DriverHostActor/VirtualTagHostActor/DiscoveredNodeMapper), all WP3-owned (the composer does not use it) — deleting it here would red the base. WP3 sweeps it in Wave B. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
|
||||
/// <summary>
|
||||
/// Which of the two v3 OPC UA namespaces a node lives in. Carried explicitly alongside a
|
||||
/// node's <c>s=</c> identifier at every address-space sink seam so the namespace is chosen at
|
||||
/// the call site, never parsed back out of the id string (explicit beats inferred).
|
||||
/// <see cref="V3NodeIds.NamespaceUri"/> maps each realm to its namespace URI.
|
||||
/// </summary>
|
||||
public enum AddressSpaceRealm
|
||||
{
|
||||
/// <summary>The device-oriented Raw tree (Folder→Driver→Device→TagGroup→Tag); a node's
|
||||
/// <c>s=</c> id is its <see cref="ZB.MOM.WW.OtOpcUa.Commons.Types.RawPaths">RawPath</see>.</summary>
|
||||
Raw,
|
||||
|
||||
/// <summary>The equipment-oriented UNS tree (Area→Line→Equipment→signal); a node's <c>s=</c>
|
||||
/// id is the slash-joined <c>Area/Line/Equipment[/EffectiveName]</c>.</summary>
|
||||
Uns,
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
|
||||
/// <summary>
|
||||
/// v3 dual-namespace NodeId + namespace-URI authority. Two OPC UA namespaces expose the same
|
||||
/// underlying values under two identity schemes:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Raw</b> (<see cref="RawNamespaceUri"/>): the device-oriented tree
|
||||
/// Folder→Driver→Device→TagGroup→Tag. A node's <c>s=</c> identifier IS its
|
||||
/// <see cref="RawPaths">RawPath</see> — folders/drivers/devices/groups included (each keys
|
||||
/// on its own RawPath prefix).</item>
|
||||
/// <item><b>Uns</b> (<see cref="UnsNamespaceUri"/>): the equipment-oriented tree
|
||||
/// Area→Line→Equipment→signal. A node's <c>s=</c> identifier is the slash-joined
|
||||
/// <c>Area/Line/Equipment[/EffectiveName]</c>.</item>
|
||||
/// </list>
|
||||
/// These replace the single <c>https://zb.com/otopcua/ns</c> namespace and the retired
|
||||
/// <c>EquipmentNodeIds</c> ({equipmentId}/{folderPath}/{name}) scheme. Realm travels alongside
|
||||
/// the identifier as an <see cref="AddressSpaceRealm"/> at every sink seam — the namespace is
|
||||
/// never parsed back out of the id string. Both schemes share <see cref="RawPaths.Separator"/>
|
||||
/// and its ordinal, case-sensitive segment charset so identity is enforced in one place.
|
||||
/// </summary>
|
||||
public static class V3NodeIds
|
||||
{
|
||||
/// <summary>The Raw namespace URI (device-oriented subtree).</summary>
|
||||
public const string RawNamespaceUri = "https://zb.com/otopcua/raw";
|
||||
|
||||
/// <summary>The UNS namespace URI (equipment-oriented subtree).</summary>
|
||||
public const string UnsNamespaceUri = "https://zb.com/otopcua/uns";
|
||||
|
||||
/// <summary>The namespace URI for a realm.</summary>
|
||||
/// <param name="realm">The address-space realm.</param>
|
||||
/// <returns>The realm's namespace URI.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Unknown realm.</exception>
|
||||
public static string NamespaceUri(AddressSpaceRealm realm) => realm switch
|
||||
{
|
||||
AddressSpaceRealm.Raw => RawNamespaceUri,
|
||||
AddressSpaceRealm.Uns => UnsNamespaceUri,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(realm), realm, "Unknown address-space realm."),
|
||||
};
|
||||
|
||||
// ----- Raw realm -----
|
||||
|
||||
/// <summary>
|
||||
/// The Raw-namespace <c>s=</c> identifier for a raw node — identical to its
|
||||
/// <see cref="RawPaths">RawPath</see> (folders, drivers, devices, groups, and tags all key
|
||||
/// on their own RawPath). A pass-through seam so call sites read intent rather than a bare
|
||||
/// string; the argument is expected to already be a RawPaths-built path.
|
||||
/// </summary>
|
||||
/// <param name="rawPath">The (already-built) RawPath.</param>
|
||||
/// <returns>The Raw-namespace <c>s=</c> identifier (== <paramref name="rawPath"/>).</returns>
|
||||
public static string Raw(string rawPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(rawPath);
|
||||
return rawPath;
|
||||
}
|
||||
|
||||
// ----- Uns realm -----
|
||||
|
||||
/// <summary>
|
||||
/// The UNS-namespace <c>s=</c> identifier for a folder or variable, slash-joined from
|
||||
/// ordered segments (Area, then Line, then Equipment, then an optional EffectiveName).
|
||||
/// Every segment is validated via <see cref="RawPaths.ValidateSegment"/> (no embedded
|
||||
/// separator, no leading/trailing whitespace) so a UNS id round-trips like a RawPath.
|
||||
/// </summary>
|
||||
/// <param name="segments">The ordered, non-empty segments (Area → leaf).</param>
|
||||
/// <returns>The slash-joined UNS <c>s=</c> identifier.</returns>
|
||||
/// <exception cref="ArgumentException">A segment is invalid, or the sequence is empty.</exception>
|
||||
public static string Uns(params string[] segments) => UnsFromSegments(segments);
|
||||
|
||||
/// <summary>Build a UNS <c>s=</c> identifier from ordered segments. See <see cref="Uns(string[])"/>.</summary>
|
||||
/// <param name="segments">The ordered, non-empty segments (Area → leaf).</param>
|
||||
/// <returns>The slash-joined UNS <c>s=</c> identifier.</returns>
|
||||
public static string Uns(IEnumerable<string> segments) => UnsFromSegments(segments);
|
||||
|
||||
/// <summary>
|
||||
/// Append a child segment (a Line under an Area, an Equipment under a Line, an
|
||||
/// EffectiveName under an Equipment) to an already-built UNS path.
|
||||
/// </summary>
|
||||
/// <param name="parentUnsPath">The parent UNS path (assumed already valid).</param>
|
||||
/// <param name="childSegment">The child segment to append.</param>
|
||||
/// <returns>The combined UNS <c>s=</c> identifier.</returns>
|
||||
/// <exception cref="ArgumentException">The child segment is invalid, or the parent is blank.</exception>
|
||||
public static string UnsChild(string parentUnsPath, string childSegment)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(parentUnsPath);
|
||||
var error = RawPaths.ValidateSegment(childSegment);
|
||||
if (error is not null)
|
||||
throw new ArgumentException($"Invalid UNS NodeId segment ('{childSegment}'): {error}", nameof(childSegment));
|
||||
|
||||
return parentUnsPath + RawPaths.Separator + childSegment;
|
||||
}
|
||||
|
||||
private static string UnsFromSegments(IEnumerable<string> segments)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(segments);
|
||||
var list = segments as IReadOnlyList<string> ?? segments.ToList();
|
||||
if (list.Count == 0)
|
||||
throw new ArgumentException("A UNS NodeId must have at least one segment.", nameof(segments));
|
||||
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
var error = RawPaths.ValidateSegment(list[i]);
|
||||
if (error is not null)
|
||||
throw new ArgumentException($"Invalid UNS NodeId segment at index {i} ('{list[i]}'): {error}", nameof(segments));
|
||||
}
|
||||
|
||||
return string.Join(RawPaths.Separator, list);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user