From 3a53e7059bcff730111a13d5451edbf0e82cd3d4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 06:05:06 -0400 Subject: [PATCH] feat(commons): ActorNameRules mirroring Akka path-element validation --- .../Validation/ActorNameRules.cs | 62 +++++++++++++++++++ .../Validation/ActorNameRulesTests.cs | 39 ++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Commons/Validation/ActorNameRules.cs create mode 100644 tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/ActorNameRulesTests.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Validation/ActorNameRules.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Validation/ActorNameRules.cs new file mode 100644 index 00000000..c946c184 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Validation/ActorNameRules.cs @@ -0,0 +1,62 @@ +using System.Text; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Validation; + +/// +/// Mirrors Akka.NET's ActorPath.IsValidPathElement (Commons does not reference Akka) so +/// entity names that become actor names can be rejected at save time instead of crashing +/// the runtime when the actor is created. +/// +public static class ActorNameRules +{ + private const string AllowedSymbols = "-_.*$+:@&=,!~';"; + + /// True when is a legal actor path element. + public static bool IsValid(string? name) + { + if (string.IsNullOrEmpty(name) || name[0] == '$') + { + return false; + } + + for (var i = 0; i < name.Length; i++) + { + var c = name[i]; + if (IsAsciiLetterOrDigit(c) || AllowedSymbols.Contains(c)) + { + continue; + } + + // Akka permits percent-encoded octets (%HH). + if (c == '%' && i + 2 < name.Length && Uri.IsHexDigit(name[i + 1]) && Uri.IsHexDigit(name[i + 2])) + { + i += 2; + continue; + } + + return false; + } + + return true; + } + + /// A readable error for an invalid name, or null when the name is valid. + public static string? Describe(string? name) + { + if (IsValid(name)) + { + return null; + } + + if (string.IsNullOrEmpty(name)) + { + return "Name must not be empty."; + } + + return $"Name '{name}' contains characters not allowed in a runtime actor name " + + $"(allowed: letters, digits and {AllowedSymbols}); it must also not start with '$'."; + } + + private static bool IsAsciiLetterOrDigit(char c) => + (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/ActorNameRulesTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/ActorNameRulesTests.cs new file mode 100644 index 00000000..b6f6ffba --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/ActorNameRulesTests.cs @@ -0,0 +1,39 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Validation; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Validation; + +public class ActorNameRulesTests +{ + [Theory] + [InlineData("a b")] + [InlineData("Furnace #3")] + [InlineData("Ofen Süd")] + [InlineData("x/y")] + [InlineData("")] + [InlineData(null)] + [InlineData("$abc")] + [InlineData("100%")] + public void IsValid_RejectsIllegalNames(string? name) + { + Assert.False(ActorNameRules.IsValid(name)); + Assert.NotNull(ActorNameRules.Describe(name)); + } + + [Theory] + [InlineData("Furnace-3")] + [InlineData("Line_1.A")] + [InlineData("a%20b")] + public void IsValid_AcceptsLegalNames(string name) + { + Assert.True(ActorNameRules.IsValid(name)); + Assert.Null(ActorNameRules.Describe(name)); + } + + [Fact] + public void Describe_NamesTheOffendingValue() + { + var msg = ActorNameRules.Describe("Furnace #3"); + Assert.Contains("'Furnace #3'", msg); + Assert.Contains("not allowed in a runtime actor name", msg); + } +}