a25593a9c6
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65 lines
2.7 KiB
C#
65 lines
2.7 KiB
C#
using System.Text.RegularExpressions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
|
|
|
/// <summary>
|
|
/// Per Phase 7 plan decision #13, alarm messages are static-with-substitution
|
|
/// templates. The engine resolves <c>{TagPath}</c> tokens at event emission time
|
|
/// against current tag values; unresolvable tokens become <c>{?}</c> so the event
|
|
/// still fires but the operator sees where the reference broke.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Token syntax: <c>{path/with/slashes}</c>. 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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).
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class MessageTemplate
|
|
{
|
|
private static readonly Regex TokenRegex = new(@"\{([^{}]+)\}",
|
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
|
|
|
/// <summary>
|
|
/// Resolve every <c>{path}</c> token in <paramref name="template"/> using
|
|
/// <paramref name="resolveTag"/>. Tokens whose returned <see cref="DataValueSnapshot"/>
|
|
/// has a non-Good <see cref="DataValueSnapshot.StatusCode"/> or a null
|
|
/// <see cref="DataValueSnapshot.Value"/> resolve to <c>{?}</c>.
|
|
/// </summary>
|
|
public static string Resolve(string template, Func<string, DataValueSnapshot?> 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() ?? "{?}";
|
|
});
|
|
}
|
|
|
|
/// <summary>Enumerate the token paths the template references. Used at publish time to validate references exist.</summary>
|
|
public static IReadOnlyList<string> ExtractTokenPaths(string? template)
|
|
{
|
|
if (string.IsNullOrEmpty(template)) return Array.Empty<string>();
|
|
var tokens = new List<string>();
|
|
foreach (Match m in TokenRegex.Matches(template))
|
|
{
|
|
var path = m.Groups[1].Value.Trim();
|
|
if (path.Length > 0) tokens.Add(path);
|
|
}
|
|
return tokens;
|
|
}
|
|
}
|