diff --git a/src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs b/src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs
index 622a7b7..bd2d4da 100644
--- a/src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs
+++ b/src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs
@@ -4,7 +4,7 @@
public static class AsyncApiExtensions
{
- private static Regex channelAddressExpressionRegex = new Regex("{[a-zA-Z1-9_-]*}");
+ private static Regex channelAddressExpressionRegex = new Regex(@"\{[A-Za-z0-9_-]+\}");
public static bool IsChannelAddressExpression(this string address)
{
diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiComponentsRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiComponentsRules.cs
index 380ef42..e22765f 100644
--- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiComponentsRules.cs
+++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiComponentsRules.cs
@@ -11,11 +11,11 @@ public static class AsyncApiComponentsRules
///
/// The key regex.
///
- public static Regex KeyRegex = new Regex(@"^[\w\d\.\-_]+$");
+ public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$");
///
/// All the fixed fields declared above are objects
- /// that MUST use keys that match the regular expression: ^[\w\d\.\-_]+$.
+ /// that MUST use keys that match the regular expression: ^[a-zA-Z0-9\.\-_]+$.
///
public static ValidationRule KeyMustBeRegularExpression =>
new ValidationRule(
diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs
index b96f94d..b5c911d 100644
--- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs
+++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs
@@ -16,6 +16,8 @@ public static class AsyncApiDocumentRules
///
private static Regex keyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, RegexTimeout);
+ private static Regex serverKeyRegex = new Regex(@"^[a-zA-Z0-9_\-]+\z", RegexOptions.None, RegexTimeout);
+
public static ValidationRule DocumentRequiredFields =>
new ValidationRule(
(context, document) =>
@@ -67,11 +69,11 @@ public static class AsyncApiDocumentRules
context.Enter("servers");
foreach (var key in document.Servers?.Keys)
{
- if (!keyRegex.IsMatch(key))
+ if (!serverKeyRegex.IsMatch(key))
{
context.CreateError(
"ServerKeys",
- string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "servers", keyRegex.ToString()));
+ string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "servers", serverKeyRegex.ToString()));
}
}
diff --git a/test/ByteBard.AsyncAPI.Tests/Models/ChannelAddressExpressionTests.cs b/test/ByteBard.AsyncAPI.Tests/Models/ChannelAddressExpressionTests.cs
new file mode 100644
index 0000000..af5be0b
--- /dev/null
+++ b/test/ByteBard.AsyncAPI.Tests/Models/ChannelAddressExpressionTests.cs
@@ -0,0 +1,49 @@
+namespace ByteBard.AsyncAPI.Tests.Models;
+
+using ByteBard.AsyncAPI.Extensions;
+using ByteBard.AsyncAPI.Models;
+using ByteBard.AsyncAPI.Readers;
+using FluentAssertions;
+using NUnit.Framework;
+
+public class ChannelAddressExpressionTests
+{
+ [TestCase("id")]
+ [TestCase("id0")]
+ [TestCase("user_id")]
+ [TestCase("user-id")]
+ public void NamedExpression_PreservesParametersWhenSerialized(string name)
+ {
+ var channel = new AsyncApiChannel { Address = "orders/{" + name + "}" };
+ channel.Parameters.Add(name, new AsyncApiParameter { Description = "Order identifier" });
+
+ channel.Address.IsChannelAddressExpression().Should().BeTrue();
+ var yaml = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);
+
+ yaml.Should().Contain("parameters:");
+ yaml.Should().Contain("description: Order identifier");
+
+ var roundTripped = new AsyncApiStringReader().ReadFragment(
+ yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostic);
+
+ diagnostic.Errors.Should().BeEmpty();
+ roundTripped.Parameters.Should().ContainKey(name);
+ roundTripped.Address.Should().Be(channel.Address);
+ }
+
+ [TestCase(null)]
+ [TestCase("")]
+ [TestCase("orders")]
+ [TestCase("orders/{}")]
+ [TestCase("orders/{user.id}")]
+ [TestCase("orders/{用户}")]
+ [TestCase("orders/{user id}")]
+ [TestCase("orders/{user/id}")]
+ [TestCase("orders/{user%20id}")]
+ [TestCase("orders/{id")]
+ [TestCase("orders/id}")]
+ public void AddressWithoutNamedExpression_IsNotRecognized(string? address)
+ {
+ address.IsChannelAddressExpression().Should().BeFalse();
+ }
+}
diff --git a/test/ByteBard.AsyncAPI.Tests/Validation/KeyRegexTests.cs b/test/ByteBard.AsyncAPI.Tests/Validation/KeyRegexTests.cs
new file mode 100644
index 0000000..a8bc733
--- /dev/null
+++ b/test/ByteBard.AsyncAPI.Tests/Validation/KeyRegexTests.cs
@@ -0,0 +1,121 @@
+namespace ByteBard.AsyncAPI.Tests.Validation;
+
+using System.Collections.Generic;
+using ByteBard.AsyncAPI.Readers;
+using FluentAssertions;
+using NUnit.Framework;
+
+public class KeyRegexTests
+{
+ [TestCase("chat.v1")]
+ [TestCase("chat_01")]
+ [TestCase("chat-1")]
+ public void ChannelKey_RetainsExistingPattern(string key)
+ {
+ var input = $$"""
+ asyncapi: 3.0.0
+ info:
+ title: Key validation
+ version: '1.0'
+ channels:
+ '{{key}}': {}
+ """;
+
+ var document = new AsyncApiStringReader().Read(input, out var diagnostic);
+
+ diagnostic.Errors.Should().BeEmpty();
+ document.Channels.Should().ContainKey(key);
+ }
+
+ public static IEnumerable ServerKeys()
+ {
+ foreach (var version in new[] { "2.6.0", "3.0.0" })
+ {
+ foreach (var key in new[] { "production", "Server_01", "server-1", "0" })
+ {
+ yield return new TestCaseData(version, key, true);
+ }
+
+ foreach (var key in new[] { "server.one", "sérver", "server/name", "server name", "", "server\n", "server\r\n", "server\nother" })
+ {
+ yield return new TestCaseData(version, key, false);
+ }
+ }
+ }
+
+ public static IEnumerable ComponentKeys()
+ {
+ foreach (var version in new[] { "2.6.0", "3.0.0" })
+ {
+ foreach (var key in new[] { "Message", "message.v1", "message_01", "message-1", "0" })
+ {
+ yield return new TestCaseData(version, key, true);
+ }
+
+ foreach (var key in new[] { "méssage", "消息", "message\u0301", "message/name", "message name", "" })
+ {
+ yield return new TestCaseData(version, key, false);
+ }
+ }
+ }
+
+ [TestCaseSource(nameof(ServerKeys))]
+ public void ServerKey_UsesSpecificationPattern(string version, string key, bool valid)
+ {
+ var addressField = version == "2.6.0" ? "url" : "host";
+ var yamlKey = key.Replace("\r", "\\r").Replace("\n", "\\n");
+ var input = $$"""
+ asyncapi: {{version}}
+ info:
+ title: Key validation
+ version: '1.0'
+ channels:
+ chat: {}
+ servers:
+ "{{yamlKey}}":
+ {{addressField}}: localhost
+ protocol: kafka
+ """;
+
+ var document = new AsyncApiStringReader().Read(input, out var diagnostic);
+
+ document.Servers.Should().ContainKey(key);
+ if (valid)
+ {
+ diagnostic.Errors.Should().BeEmpty();
+ }
+ else
+ {
+ diagnostic.Errors.Should().ContainSingle(e => e.Message.Contains("in 'servers' MUST match"));
+ }
+ }
+
+ [TestCaseSource(nameof(ComponentKeys))]
+ public void ComponentKey_UsesAsciiSpecificationPattern(string version, string key, bool valid)
+ {
+ var input = $$"""
+ asyncapi: {{version}}
+ info:
+ title: Key validation
+ version: '1.0'
+ channels:
+ chat: {}
+ components:
+ messages:
+ '{{key}}':
+ name: example
+ """;
+
+ var document = new AsyncApiStringReader().Read(input, out var diagnostic);
+
+ document.Components.Messages.Should().ContainKey(key);
+ if (valid)
+ {
+ diagnostic.Errors.Should().BeEmpty();
+ }
+ else
+ {
+ diagnostic.Errors.Should().ContainSingle(e => e.Message.Contains("in 'messages' MUST match"));
+ }
+ }
+}