namespace ZB.MOM.WW.OtOpcUa.Cluster; public static class RoleParser { /// The admin-role name. public const string Admin = "admin"; /// The driver-role name. Single source of truth — downstream DriverRole /// constants (RedundancyStateActor, ClusterNodeAddressReconcilerActor, /// ServiceCollectionExtensions) alias this value rather than duplicating the literal. public const string Driver = "driver"; /// The dev-role name. public const string Dev = "dev"; /// Prefix identifying a cluster-scoped role, e.g. cluster-SITE-A (per-cluster mesh, Phase 6). public const string ClusterRolePrefix = "cluster-"; private static readonly HashSet Allowed = new(StringComparer.Ordinal) { Admin, Driver, Dev, }; /// Returns true when is a cluster-scoped role — starts with /// and carries a non-empty ClusterId suffix. /// The role name to test. public static bool IsClusterRole(string role) => role.StartsWith(ClusterRolePrefix, StringComparison.Ordinal) && role.Length > ClusterRolePrefix.Length; /// Extracts the ClusterId from a cluster-scoped role, e.g. cluster-SITE-ASITE-A. /// Assumes is true for . /// The cluster-scoped role name. public static string ClusterIdFromRole(string role) => role[ClusterRolePrefix.Length..]; /// Parses a comma-separated string of role names into a validated array. /// The raw role string to parse. /// The distinct, lower-cased, validated role names. public static string[] Parse(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return Array.Empty(); var roles = raw .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(r => r.ToLowerInvariant()) .Distinct() .ToArray(); foreach (var r in roles) { if (!Allowed.Contains(r) && !IsClusterRole(r)) throw new ArgumentException( $"Unknown role '{r}'. Allowed: {string.Join(", ", Allowed)}, or '{ClusterRolePrefix}'.", nameof(raw)); } return roles; } }