using System.Text.Json; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// How an attribute change should be routed to a child (script/alarm) actor. internal enum TriggerRoutingKind { /// Reacts only to changes of a single named attribute. MonitoredAttribute, /// Reacts to ANY attribute change (Expression triggers; or fail-open when the /// trigger config is unparseable — the child's own gate then discards what it ignores). AllChanges, /// Never reacts to attribute changes (interval / call-only scripts). None } /// /// Result of classifying a child's trigger for attribute-change routing. /// internal readonly record struct TriggerRoutingDecision(TriggerRoutingKind Kind, string? MonitoredAttribute) { public static readonly TriggerRoutingDecision AllChanges = new(TriggerRoutingKind.AllChanges, null); public static readonly TriggerRoutingDecision None = new(TriggerRoutingKind.None, null); /// Creates a decision routing on changes to a single named attribute. /// The name of the attribute to monitor. /// A of kind . public static TriggerRoutingDecision Monitored(string attribute) => new(TriggerRoutingKind.MonitoredAttribute, attribute); } /// /// Single source of truth for the monitored attribute a script/alarm trigger reacts to (P2). /// Shared by (attribute-name extraction) and /// InstanceActor.CreateChildActors (routing-map construction) so both agree on the /// attribute for a given trigger config. /// /// NOTE: routing has three outcomes (monitored / all-changes / none), so this returns a /// rather than the plan's original bool-out signature — /// a bool cannot distinguish "hears everything" from "hears nothing". /// internal static class TriggerRouting { /// /// Reads the monitored attribute name from a trigger config, honoring both the /// "attributeName" (scripts + alarms) and "attribute" (alarm alias) keys. /// Returns false when the JSON is missing, malformed, or carries no attribute name. /// /// The trigger config JSON to read from. /// When this method returns, the monitored attribute name, or an empty string if not found. /// true if an attribute name was found; otherwise false. public static bool TryReadAttributeName(string? triggerConfigJson, out string attribute) { attribute = ""; if (string.IsNullOrEmpty(triggerConfigJson)) return false; try { using var doc = JsonDocument.Parse(triggerConfigJson); var root = doc.RootElement; if (root.TryGetProperty("attributeName", out var a) && a.GetString() is { Length: > 0 } n) { attribute = n; return true; } if (root.TryGetProperty("attribute", out var a2) && a2.GetString() is { Length: > 0 } n2) { attribute = n2; return true; } return false; } catch (JsonException) { return false; } } /// /// Classifies a script trigger. valuechange/conditional → the named attribute (fail open to /// all-changes if unparseable); expression → all-changes; interval/call → none; anything /// unknown → all-changes (fail open — a misconfigured script still reacts, its own gate /// remains the correctness check). /// /// The script's trigger type (e.g. "valuechange", "expression", "interval", "call"). /// The trigger config JSON, used to extract the monitored attribute name where applicable. /// The routing decision for the script's trigger. public static TriggerRoutingDecision ForScript(string? triggerType, string? triggerConfigJson) { switch (triggerType?.ToLowerInvariant()) { case "valuechange": case "conditional": return TryReadAttributeName(triggerConfigJson, out var attr) ? TriggerRoutingDecision.Monitored(attr) : TriggerRoutingDecision.AllChanges; case "expression": return TriggerRoutingDecision.AllChanges; case "interval": case "call": return TriggerRoutingDecision.None; default: return TriggerRoutingDecision.AllChanges; } } /// /// Classifies an alarm trigger. Expression → all-changes; HiLo/ValueMatch/RangeViolation/ /// RateOfChange → the named attribute (fail open to all-changes if unparseable); an /// unknown/unparseable trigger type → all-changes (fail open). /// /// The alarm's trigger type (e.g. "Expression", "HiLo", "ValueMatch"). /// The trigger config JSON, used to extract the monitored attribute name where applicable. /// The routing decision for the alarm's trigger. public static TriggerRoutingDecision ForAlarm(string? triggerType, string? triggerConfigJson) { if (Enum.TryParse(triggerType, ignoreCase: true, out var tt)) { switch (tt) { case AlarmTriggerType.Expression: return TriggerRoutingDecision.AllChanges; case AlarmTriggerType.HiLo: case AlarmTriggerType.ValueMatch: case AlarmTriggerType.RangeViolation: case AlarmTriggerType.RateOfChange: return TryReadAttributeName(triggerConfigJson, out var attr) ? TriggerRoutingDecision.Monitored(attr) : TriggerRoutingDecision.AllChanges; } } return TriggerRoutingDecision.AllChanges; } }