From 2ba2f8a8997c08b4bb3e5693109698cf64994f64 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 11 Jun 2026 20:59:10 -0400 Subject: [PATCH] =?UTF-8?q?feat(commons):=20TryParseRelayBody=20=E2=80=94?= =?UTF-8?q?=20detect=20pure=20ctx.GetTag=20relay=20scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Types/EquipmentScriptPaths.cs | 33 +++++++++++++++++++ .../EquipmentScriptPathsTests.cs | 25 ++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs index 6128c6dd..4c9e95ce 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs @@ -32,6 +32,13 @@ public static class EquipmentScriptPaths private static readonly Regex PathLiteralRegex = new(@"(ctx\s*\.\s*(?:GetTag|SetVirtualTag)\s*\(\s*"")([^""]*)("")", RegexOptions.Compiled); + // A pure pass-through virtual-tag body: exactly `return ctx.GetTag("").Value;` + // (whitespace-insensitive). Captures for relay→alias conversion. Anything with extra + // statements, arithmetic, a different member than .Value, or multiple GetTag calls is NOT a relay. + private static readonly Regex RelayBodyRegex = new( + @"^\s*return\s+ctx\s*\.\s*GetTag\s*\(\s*""([^""]+)""\s*\)\s*\.\s*Value\s*;\s*$", + RegexOptions.Compiled); + /// True when the source uses the {{equip}} token anywhere. /// The script source to scan. public static bool ContainsEquipToken(string? source) => @@ -136,4 +143,30 @@ public static class EquipmentScriptPaths return result; } + + /// + /// Recognise a pure relay virtual-tag body — one whose entire body is exactly + /// return ctx.GetTag("").Value; (whitespace-insensitive) — + /// and capture its single GetTag reference. Bodies with extra statements, arithmetic, + /// a member other than .Value, or multiple GetTag calls are not relays. + /// + /// The virtual-tag script source to inspect. + /// + /// When the method returns , the captured GetTag path literal + /// (e.g. TestMachine_020.TestChangingInt or {{equip}}.Speed); + /// otherwise . + /// + /// + /// when is a pure pass-through relay; + /// for any other body or / input. + /// + public static bool TryParseRelayBody(string? source, out string? tagReference) + { + tagReference = null; + if (string.IsNullOrWhiteSpace(source)) return false; + var m = RelayBodyRegex.Match(source); + if (!m.Success) return false; + tagReference = m.Groups[1].Value; + return true; + } } diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/EquipmentScriptPathsTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/EquipmentScriptPathsTests.cs index 27c00ac4..b1a9e8da 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/EquipmentScriptPathsTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/EquipmentScriptPathsTests.cs @@ -211,4 +211,29 @@ public class EquipmentScriptPathsTests { EquipmentScriptPaths.ContainsEquipToken(null).ShouldBeFalse(); } + + // ---- TryParseRelayBody ---- + + [Theory] + [InlineData("return ctx.GetTag(\"TestMachine_020.TestChangingInt\").Value;", "TestMachine_020.TestChangingInt")] + [InlineData(" return ctx . GetTag ( \"A.B\" ) . Value ; ", "A.B")] + [InlineData("return ctx.GetTag(\"{{equip}}.Speed\").Value;", "{{equip}}.Speed")] + public void TryParseRelayBody_accepts_exact_passthrough(string src, string expectedRef) + { + EquipmentScriptPaths.TryParseRelayBody(src, out var r).ShouldBeTrue(); + r.ShouldBe(expectedRef); + } + + [Theory] + [InlineData("return ctx.GetTag(\"A.B\").Value * 2;")] + [InlineData("var x = ctx.GetTag(\"A.B\").Value; return x;")] + [InlineData("return ctx.GetTag(\"A.B\").Value + ctx.GetTag(\"C.D\").Value;")] + [InlineData("return ctx.GetTag(\"A.B\").Quality;")] + [InlineData("return 5;")] + [InlineData("")] + public void TryParseRelayBody_rejects_non_relay(string src) + { + EquipmentScriptPaths.TryParseRelayBody(src, out var r).ShouldBeFalse(); + r.ShouldBeNull(); + } }