Skip to content

Commit 9ebf225

Browse files
committed
fix: align identifier validation with spec constraints
Use URL-safe ASCII identifier keys (letters, digits, dots, underscores, and hyphens) as the library baseline, with stricter rules wherever the specification requires them. - Reject dots in server keys. - Restrict component keys to ASCII. - Preserve the existing channel-key restriction. - Correct channel-address expression detection to include 0 and require nonempty names. - Add regression coverage for validation and parameter serialization. Compatibility: dotted server keys and Unicode component keys now produce validation errors, while their values remain preserved in the model.
1 parent cc1e7a5 commit 9ebf225

5 files changed

Lines changed: 176 additions & 5 deletions

File tree

‎src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
public static class AsyncApiExtensions
66
{
7-
private static Regex channelAddressExpressionRegex = new Regex("{[a-zA-Z1-9_-]*}");
7+
private static Regex channelAddressExpressionRegex = new Regex(@"\{[A-Za-z0-9_-]+\}");
88

99
public static bool IsChannelAddressExpression(this string address)
1010
{

‎src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiComponentsRules.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ public static class AsyncApiComponentsRules
1111
/// <summary>
1212
/// The key regex.
1313
/// </summary>
14-
public static Regex KeyRegex = new Regex(@"^[\w\d\.\-_]+$");
14+
public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$");
1515

1616
/// <summary>
1717
/// All the fixed fields declared above are objects
18-
/// that MUST use keys that match the regular expression: ^[\w\d\.\-_]+$.
18+
/// that MUST use keys that match the regular expression: ^[a-zA-Z0-9\.\-_]+$.
1919
/// </summary>
2020
public static ValidationRule<AsyncApiComponents> KeyMustBeRegularExpression =>
2121
new ValidationRule<AsyncApiComponents>(

‎src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public static class AsyncApiDocumentRules
1616
/// </summary>
1717
private static Regex keyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, RegexTimeout);
1818

19+
private static Regex serverKeyRegex = new Regex(@"^[a-zA-Z0-9_\-]+$", RegexOptions.None, RegexTimeout);
20+
1921
public static ValidationRule<AsyncApiDocument> DocumentRequiredFields =>
2022
new ValidationRule<AsyncApiDocument>(
2123
(context, document) =>
@@ -67,11 +69,11 @@ public static class AsyncApiDocumentRules
6769
context.Enter("servers");
6870
foreach (var key in document.Servers?.Keys)
6971
{
70-
if (!keyRegex.IsMatch(key))
72+
if (!serverKeyRegex.IsMatch(key))
7173
{
7274
context.CreateError(
7375
"ServerKeys",
74-
string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "servers", keyRegex.ToString()));
76+
string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "servers", serverKeyRegex.ToString()));
7577
}
7678
}
7779

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
namespace ByteBard.AsyncAPI.Tests.Models;
2+
3+
using ByteBard.AsyncAPI.Extensions;
4+
using ByteBard.AsyncAPI.Models;
5+
using ByteBard.AsyncAPI.Readers;
6+
using FluentAssertions;
7+
using NUnit.Framework;
8+
9+
public class ChannelAddressExpressionTests
10+
{
11+
[TestCase("id")]
12+
[TestCase("id0")]
13+
[TestCase("user_id")]
14+
[TestCase("user-id")]
15+
public void NamedExpression_PreservesParametersWhenSerialized(string name)
16+
{
17+
var channel = new AsyncApiChannel { Address = "orders/{" + name + "}" };
18+
channel.Parameters.Add(name, new AsyncApiParameter { Description = "Order identifier" });
19+
20+
channel.Address.IsChannelAddressExpression().Should().BeTrue();
21+
var yaml = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);
22+
23+
yaml.Should().Contain("parameters:");
24+
yaml.Should().Contain("description: Order identifier");
25+
26+
var roundTripped = new AsyncApiStringReader().ReadFragment<AsyncApiChannel>(
27+
yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostic);
28+
29+
diagnostic.Errors.Should().BeEmpty();
30+
roundTripped.Parameters.Should().ContainKey(name);
31+
roundTripped.Address.Should().Be(channel.Address);
32+
}
33+
34+
[TestCase(null)]
35+
[TestCase("")]
36+
[TestCase("orders")]
37+
[TestCase("orders/{}")]
38+
[TestCase("orders/{user.id}")]
39+
[TestCase("orders/{用户}")]
40+
[TestCase("orders/{user id}")]
41+
[TestCase("orders/{user/id}")]
42+
[TestCase("orders/{user%20id}")]
43+
[TestCase("orders/{id")]
44+
[TestCase("orders/id}")]
45+
public void AddressWithoutNamedExpression_IsNotRecognized(string? address)
46+
{
47+
address.IsChannelAddressExpression().Should().BeFalse();
48+
}
49+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
namespace ByteBard.AsyncAPI.Tests.Validation;
2+
3+
using System.Collections.Generic;
4+
using ByteBard.AsyncAPI.Readers;
5+
using FluentAssertions;
6+
using NUnit.Framework;
7+
8+
public class KeyRegexTests
9+
{
10+
[TestCase("chat.v1")]
11+
[TestCase("chat_01")]
12+
[TestCase("chat-1")]
13+
public void ChannelKey_RetainsExistingPattern(string key)
14+
{
15+
var input = $$"""
16+
asyncapi: 3.0.0
17+
info:
18+
title: Key validation
19+
version: '1.0'
20+
channels:
21+
'{{key}}': {}
22+
""";
23+
24+
var document = new AsyncApiStringReader().Read(input, out var diagnostic);
25+
26+
diagnostic.Errors.Should().BeEmpty();
27+
document.Channels.Should().ContainKey(key);
28+
}
29+
30+
public static IEnumerable<TestCaseData> ServerKeys()
31+
{
32+
foreach (var version in new[] { "2.6.0", "3.0.0" })
33+
{
34+
foreach (var key in new[] { "production", "Server_01", "server-1", "0" })
35+
{
36+
yield return new TestCaseData(version, key, true);
37+
}
38+
39+
foreach (var key in new[] { "server.one", "sérver", "server/name", "server name", "" })
40+
{
41+
yield return new TestCaseData(version, key, false);
42+
}
43+
}
44+
}
45+
46+
public static IEnumerable<TestCaseData> ComponentKeys()
47+
{
48+
foreach (var version in new[] { "2.6.0", "3.0.0" })
49+
{
50+
foreach (var key in new[] { "Message", "message.v1", "message_01", "message-1", "0" })
51+
{
52+
yield return new TestCaseData(version, key, true);
53+
}
54+
55+
foreach (var key in new[] { "méssage", "消息", "message\u0301", "message/name", "message name", "" })
56+
{
57+
yield return new TestCaseData(version, key, false);
58+
}
59+
}
60+
}
61+
62+
[TestCaseSource(nameof(ServerKeys))]
63+
public void ServerKey_UsesSpecificationPattern(string version, string key, bool valid)
64+
{
65+
var addressField = version == "2.6.0" ? "url" : "host";
66+
var input = $$"""
67+
asyncapi: {{version}}
68+
info:
69+
title: Key validation
70+
version: '1.0'
71+
channels:
72+
chat: {}
73+
servers:
74+
'{{key}}':
75+
{{addressField}}: localhost
76+
protocol: kafka
77+
""";
78+
79+
var document = new AsyncApiStringReader().Read(input, out var diagnostic);
80+
81+
document.Servers.Should().ContainKey(key);
82+
if (valid)
83+
{
84+
diagnostic.Errors.Should().BeEmpty();
85+
}
86+
else
87+
{
88+
diagnostic.Errors.Should().ContainSingle(e => e.Message.Contains("in 'servers' MUST match"));
89+
}
90+
}
91+
92+
[TestCaseSource(nameof(ComponentKeys))]
93+
public void ComponentKey_UsesAsciiSpecificationPattern(string version, string key, bool valid)
94+
{
95+
var input = $$"""
96+
asyncapi: {{version}}
97+
info:
98+
title: Key validation
99+
version: '1.0'
100+
channels:
101+
chat: {}
102+
components:
103+
messages:
104+
'{{key}}':
105+
name: example
106+
""";
107+
108+
var document = new AsyncApiStringReader().Read(input, out var diagnostic);
109+
110+
document.Components.Messages.Should().ContainKey(key);
111+
if (valid)
112+
{
113+
diagnostic.Errors.Should().BeEmpty();
114+
}
115+
else
116+
{
117+
diagnostic.Errors.Should().ContainSingle(e => e.Message.Contains("in 'messages' MUST match"));
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)