using System.Text.RegularExpressions; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; /// /// Per Phase 7 plan decision #13, alarm messages are static-with-substitution /// templates. The engine resolves {TagPath} tokens at event emission time /// against current tag values; unresolvable tokens become {?} so the event /// still fires but the operator sees where the reference broke. /// /// /// /// Token syntax: {path/with/slashes}. Brace-stripped the contents must /// match a path the caller's resolver function can look up. No escaping /// currently — if you need literal braces in the message, reach for a feature /// request. /// /// /// Pure function. Same inputs always produce the same string. Tests verify the /// edge cases (no tokens / one token / many / nested / unresolvable / bad /// quality / null value). /// /// public static class MessageTemplate { private static readonly Regex TokenRegex = new(@"\{([^{}]+)\}", RegexOptions.Compiled | RegexOptions.CultureInvariant); /// /// Resolve every {path} token in using /// . Tokens whose returned /// has a non-Good or a null /// resolve to {?}. /// /// /// Quality bar is intentionally stricter than predicate evaluation: /// only Good (StatusCode == 0) is substituted; Uncertain renders as /// {?}. The predicate gate (ScriptedAlarmEngine.AreInputsReady) /// accepts Uncertain because it still carries a value the predicate can /// inspect, but the operator-facing message must make doubt explicit rather /// than substituting a value an operator might act on. See the /// "Input-quality policy" section in docs/ScriptedAlarms.md. /// (Core.ScriptedAlarms-010) /// /// The template string with {path} tokens. /// A function to resolve tag values by path. /// The resolved template string with tokens replaced or marked as unresolvable. public static string Resolve(string template, Func resolveTag) { if (string.IsNullOrEmpty(template)) return template ?? string.Empty; if (resolveTag is null) throw new ArgumentNullException(nameof(resolveTag)); return TokenRegex.Replace(template, match => { var path = match.Groups[1].Value.Trim(); if (path.Length == 0) return "{?}"; var snap = resolveTag(path); if (snap is null) return "{?}"; if (snap.StatusCode != 0u) return "{?}"; return snap.Value?.ToString() ?? "{?}"; }); } /// Enumerate the token paths the template references. Used at publish time to validate references exist. /// The template string to extract tokens from. /// A list of all token paths found in the template. public static IReadOnlyList ExtractTokenPaths(string? template) { if (string.IsNullOrEmpty(template)) return Array.Empty(); var tokens = new List(); foreach (Match m in TokenRegex.Matches(template)) { var path = m.Groups[1].Value.Trim(); if (path.Length > 0) tokens.Add(path); } return tokens; } }