826312befe
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.
81 lines
4.1 KiB
C#
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());
|
|
}
|
|
}
|