From 952b41f0f76146c41eb43b59de059d3753440dcb Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 8 Sep 2026 01:07:38 -0400 Subject: [PATCH] =?UTF-8?q?feat(commons):=20ScriptTriggerAttribute=20?= =?UTF-8?q?=E2=80=94=20monitored=20attribute=20of=20a=20ValueChange/Condit?= =?UTF-8?q?ional=20trigger=20(flow=20debugger=20script=20entry)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29 --- .../Types/Scripts/ScriptTriggerAttribute.cs | 49 +++++++++++++++++++ .../Scripts/ScriptTriggerAttributeTests.cs | 46 +++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptTriggerAttribute.cs create mode 100644 tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Scripts/ScriptTriggerAttributeTests.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptTriggerAttribute.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptTriggerAttribute.cs new file mode 100644 index 00000000..c9f371b6 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptTriggerAttribute.cs @@ -0,0 +1,49 @@ +using System.Text.Json; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts; + +/// +/// Reads the single monitored attribute of a ValueChange or Conditional script +/// trigger (Flow Debugger script entry, design 2026-09-08 §2). The config keys are the ones the +/// Central UI's ScriptTriggerConfigCodec writes (attributeName) and SiteRuntime's +/// TriggerRouting also accepts (attribute). Pure; never throws on bad JSON. +/// +public static class ScriptTriggerAttribute +{ + /// True for the trigger kinds the Flow Debugger fires by WRITING the monitored attribute. + public static bool IsWriteKind(string? triggerType) => + string.Equals(triggerType, "ValueChange", StringComparison.OrdinalIgnoreCase) + || string.Equals(triggerType, "Conditional", StringComparison.OrdinalIgnoreCase); + + /// + /// Returns the monitored attribute for a write-kind trigger with a readable configuration; + /// false (and null) for every other kind, and for a write kind whose config is missing, + /// malformed, not an object, or names no attribute. + /// + public static bool TryRead(string? triggerType, string? triggerConfigurationJson, out string? attribute) + { + attribute = null; + if (!IsWriteKind(triggerType) || string.IsNullOrWhiteSpace(triggerConfigurationJson)) + return false; + try + { + using var doc = JsonDocument.Parse(triggerConfigurationJson); + var root = doc.RootElement; + if (root.ValueKind != JsonValueKind.Object) return false; + foreach (var key in new[] { "attributeName", "attribute" }) + { + if (root.TryGetProperty(key, out var v) && v.ValueKind == JsonValueKind.String + && v.GetString() is { Length: > 0 } name) + { + attribute = name; + return true; + } + } + return false; + } + catch (JsonException) + { + return false; + } + } +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Scripts/ScriptTriggerAttributeTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Scripts/ScriptTriggerAttributeTests.cs new file mode 100644 index 00000000..2b2d14f2 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Scripts/ScriptTriggerAttributeTests.cs @@ -0,0 +1,46 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Scripts; + +/// Flow Debugger script entry (design 2026-09-08 §2): the monitored attribute of a ValueChange/Conditional trigger. +public class ScriptTriggerAttributeTests +{ + [Theory] + [InlineData("ValueChange", """{"attributeName":"Level"}""", "Level")] + [InlineData("valuechange", """{"attribute":"Level"}""", "Level")] + [InlineData("Conditional", """{"attributeName":"Level","operator":">","threshold":"5"}""", "Level")] + [InlineData("CONDITIONAL", """{"attribute":"Flag"}""", "Flag")] + public void WriteKinds_ReturnTheMonitoredAttribute(string kind, string config, string expected) + { + Assert.True(ScriptTriggerAttribute.TryRead(kind, config, out var attribute)); + Assert.Equal(expected, attribute); + Assert.True(ScriptTriggerAttribute.IsWriteKind(kind)); + } + + [Theory] + [InlineData("Call", """{"attributeName":"Level"}""")] + [InlineData("Interval", """{"intervalMs":1000}""")] + [InlineData("Expression", """{"expression":"true"}""")] + [InlineData(null, null)] + [InlineData("", "")] + public void OtherKinds_ReturnFalse_EvenWhenTheConfigNamesAnAttribute(string? kind, string? config) + { + Assert.False(ScriptTriggerAttribute.TryRead(kind, config, out var attribute)); + Assert.Null(attribute); + Assert.False(ScriptTriggerAttribute.IsWriteKind(kind)); + } + + [Theory] + [InlineData("ValueChange", null)] + [InlineData("ValueChange", "")] + [InlineData("ValueChange", "not json")] + [InlineData("ValueChange", "[1,2]")] + [InlineData("ValueChange", """{"attributeName":""}""")] + [InlineData("ValueChange", """{"other":"x"}""")] + public void WriteKind_WithoutAReadableAttribute_ReturnsFalse_ButIsStillAWriteKind(string kind, string? config) + { + Assert.False(ScriptTriggerAttribute.TryRead(kind, config, out var attribute)); + Assert.Null(attribute); + Assert.True(ScriptTriggerAttribute.IsWriteKind(kind)); + } +}