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 {?}. /// 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. 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; } }