feat(commons): ActorNameRules mirroring Akka path-element validation
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mirrors Akka.NET's <c>ActorPath.IsValidPathElement</c> (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.
|
||||||
|
/// </summary>
|
||||||
|
public static class ActorNameRules
|
||||||
|
{
|
||||||
|
private const string AllowedSymbols = "-_.*$+:@&=,!~';";
|
||||||
|
|
||||||
|
/// <summary>True when <paramref name="name"/> is a legal actor path element.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A readable error for an invalid name, or null when the name is valid.</summary>
|
||||||
|
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');
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user