Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ public static class AsyncApiComponentsRules
/// <summary>
/// The key regex.
/// </summary>
public static Regex KeyRegex = new Regex(@"^[\w\d\.\-_]+$");
public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$");

/// <summary>
/// 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\.\-_]+$.
/// </summary>
public static ValidationRule<AsyncApiComponents> KeyMustBeRegularExpression =>
new ValidationRule<AsyncApiComponents>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public static class AsyncApiDocumentRules
/// </summary>
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<AsyncApiDocument> DocumentRequiredFields =>
new ValidationRule<AsyncApiDocument>(
(context, document) =>
Expand Down Expand Up @@ -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()));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<AsyncApiChannel>(
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();
}
}
121 changes: 121 additions & 0 deletions test/ByteBard.AsyncAPI.Tests/Validation/KeyRegexTests.cs
Original file line number Diff line number Diff line change
@@ -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<TestCaseData> 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<TestCaseData> 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"));
}
}
}
Loading