Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/SparkplugRebirthScope.cs
T
Joseph Doherty 826312befe docs: fill missing XML docs and strip tracking IDs from src comments
Clears CommentChecker findings in src/ (3,280 -> 209, the remainder being false
positives like PLC-5/IEEE-754/M0.0) by adding missing summaries/params/returns,
normalising inheritdoc usage, and removing Gitea/review tracking IDs while keeping
the rationale they annotated. Comment-only: comment-stripped sources are identical
to the parent commit.
2026-10-15 06:14:58 -04:00

81 lines
4.1 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
/// <summary>
/// Validates an operator-typed Sparkplug rebirth scope for the raw browse modal.
/// </summary>
/// <remarks>
/// <para>
/// <b>Why a typed scope exists at all.</b> The Sparkplug browse tree is <i>birth-driven</i>, and
/// births are never retained. On a quiet plant — every edge node birthed long before the browse
/// window opened, and is now publishing only NDATA/DDATA — the tree comes up empty. The
/// Request-rebirth scope was derived solely from a tree selection, so the one action that would
/// populate the tree could not be armed: the action needed the tree, and the tree needed the action.
/// </para>
/// <para>
/// <b>This is advisory, not the gate.</b> <c>MqttBrowseSession.ResolveRebirthTargets</c> re-resolves
/// the scope and refuses an unusable one <em>before any message leaves</em>, and the
/// <c>DriverOperator</c> policy is enforced server-side in <c>BrowserSessionService</c>. This exists
/// so the modal does not let an operator arm a two-click confirm that cannot succeed. Being the
/// looser of the two is therefore safe; being the stricter one would block a scope the session
/// accepts, which is the failure mode the repo's authoring surfaces avoid by convention.
/// </para>
/// <para>
/// <b>A group-only scope is rejected on purpose.</b> The session resolves a bare group by enumerating
/// the edge nodes it has <i>observed</i> — of which an empty tree has none — so it could not help the
/// case this input exists for, and on a populated tree it would fan one click across the whole group.
/// The tree-selection path still offers a group scope, with its own confirm-time cap and warning.
/// </para>
/// </remarks>
public static class SparkplugRebirthScope
{
/// <summary>MQTT wildcards. A rebirth addresses one edge node exactly; a wildcard cannot name one.</summary>
private static readonly char[] Wildcards = ['+', '#'];
/// <summary>
/// Describes why <paramref name="raw"/> is not a usable <c>{group}/{edgeNode}</c> rebirth scope.
/// </summary>
/// <param name="raw">The operator-typed scope, untrimmed; <see langword="null"/> is treated as blank.</param>
/// <returns>The operator-facing error text, or <see langword="null"/> when the scope is usable.</returns>
public static string? Describe(string? raw)
{
var scope = (raw ?? "").Trim();
if (scope.Length == 0) { return "Enter the edge node as 'group/edgeNode'."; }
if (scope.IndexOfAny(Wildcards) >= 0)
{
return "An MQTT wildcard (+ or #) cannot address an edge node; name one exactly.";
}
var segments = scope.Split('/');
if (segments.Length != 2 || Array.Exists(segments, string.IsNullOrWhiteSpace))
{
return "Use exactly 'group/edgeNode' — a group on its own would ask every edge node in it to "
+ "republish, which this window cannot enumerate while the tree is empty.";
}
return null;
}
/// <summary>True when <paramref name="raw"/> is a usable rebirth scope.</summary>
/// <param name="raw">The operator-typed scope.</param>
/// <returns><see langword="true"/> when <see cref="Describe"/> finds no fault.</returns>
public static bool IsUsable(string? raw) => Describe(raw) is null;
/// <summary>
/// Splits a scope already accepted by <see cref="Describe"/> into its two segments.
/// </summary>
/// <param name="raw">A scope for which <see cref="IsUsable"/> is <see langword="true"/>.</param>
/// <returns>The trimmed <c>(group, edgeNode)</c> pair.</returns>
/// <exception cref="ArgumentException">The scope is not usable.</exception>
public static (string GroupId, string EdgeNodeId) Split(string? raw)
{
if (Describe(raw) is { } error)
{
throw new ArgumentException($"'{raw}' is not a usable rebirth scope: {error}", nameof(raw));
}
var segments = raw!.Trim().Split('/');
return (segments[0].Trim(), segments[1].Trim());
}
}