diff --git a/src/LEGO.AsyncAPI/Resource.Designer.cs b/src/LEGO.AsyncAPI/Resource.Designer.cs
index 755eda9c..b28de91a 100644
--- a/src/LEGO.AsyncAPI/Resource.Designer.cs
+++ b/src/LEGO.AsyncAPI/Resource.Designer.cs
@@ -60,6 +60,15 @@ internal Resource() {
}
}
+ ///
+ /// Looks up a localized string similar to Channel signature '{0}' MUST be unique..
+ ///
+ internal static string Validation_ChannelsMustBeUnique {
+ get {
+ return ResourceManager.GetString("Validation_ChannelsMustBeUnique", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The string '{0}' MUST be an email address..
///
diff --git a/src/LEGO.AsyncAPI/Resource.resx b/src/LEGO.AsyncAPI/Resource.resx
index 9d882464..a97d5939 100644
--- a/src/LEGO.AsyncAPI/Resource.resx
+++ b/src/LEGO.AsyncAPI/Resource.resx
@@ -117,6 +117,9 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Channel signature '{0}' MUST be unique.
+
The string '{0}' MUST be an email address.
diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs
index 27076369..d866c4df 100644
--- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs
+++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs
@@ -2,6 +2,8 @@
namespace LEGO.AsyncAPI.Validation.Rules
{
+ using System;
+ using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using LEGO.AsyncAPI.Models;
@@ -10,10 +12,13 @@ namespace LEGO.AsyncAPI.Validation.Rules
[AsyncApiRule]
public static class AsyncApiDocumentRules
{
+ private static TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
+
///
/// The key regex.
///
- public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$");
+ public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, RegexTimeout);
+ public static Regex ChannelKeyUriTemplateRegex = new Regex(@"^(?:(?:[^\x00-\x20""'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$", RegexOptions.IgnoreCase, RegexTimeout);
public static ValidationRule DocumentRequiredFields =>
new ValidationRule(
@@ -30,16 +35,58 @@ public static class AsyncApiDocumentRules
context.Exit();
context.Enter("channels");
- if (document.Channels == null || !document.Channels.Keys.Any())
+ try
{
- context.CreateError(
- nameof(DocumentRequiredFields),
- string.Format(Resource.Validation_FieldRequired, "channels", "document"));
- }
+ // MUST have at least 1 channel
+ if (document.Channels == null || !document.Channels.Keys.Any())
+ {
+ context.CreateError(
+ nameof(DocumentRequiredFields),
+ string.Format(Resource.Validation_FieldRequired, "channels", "document"));
+ return;
+ }
+ var hashSet = new HashSet();
+ foreach (var key in document.Channels.Keys)
+ {
+ // Uri-template
+ if (!ChannelKeyUriTemplateRegex.IsMatch(key))
+ {
+ context.CreateError(
+ "ChannelKeys",
+ string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "channels", KeyRegex.ToString()));
+ }
- context.Exit();
+ // Unique channel keys
+ var pathSignature = GetKeySignature(key);
+ if (!hashSet.Add(pathSignature))
+ {
+ context.CreateError("ChannelKey", string.Format(Resource.Validation_ChannelsMustBeUnique, pathSignature));
+ }
+ }
+ }
+ finally
+ {
+ context.Exit();
+ }
});
+ private static string GetKeySignature(string path)
+ {
+ for (int openBrace = path.IndexOf('{'); openBrace > -1; openBrace = path.IndexOf('{', openBrace + 2))
+ {
+ int closeBrace = path.IndexOf('}', openBrace);
+
+ if (closeBrace < 0)
+ {
+ return path;
+ }
+
+ path = path.Substring(0, openBrace + 1) + path.Substring(closeBrace);
+ }
+
+ return path;
+ }
+
public static ValidationRule KeyMustBeRegularExpression =>
new ValidationRule(
(context, document) =>
diff --git a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs
new file mode 100644
index 00000000..5d9f7bec
--- /dev/null
+++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs
@@ -0,0 +1,126 @@
+// Copyright (c) The LEGO Group. All rights reserved.
+
+namespace LEGO.AsyncAPI.Tests.Validation
+{
+ using FluentAssertions;
+ using LEGO.AsyncAPI.Readers;
+ using LEGO.AsyncAPI.Validations;
+ using NUnit.Framework;
+ using System.Linq;
+
+ public class ValidationRuleTests
+ {
+ [Test]
+ [TestCase("chat-{person-id}")]
+ public void ChannelKey_WithInvalidParameter_DiagnosticsError(string channelKey)
+ {
+ var input =
+ $"""
+ asyncapi: 2.6.0
+ info:
+ title: Chat Application
+ version: 1.0.0
+ servers:
+ testing:
+ url: test.mosquitto.org:1883
+ protocol: mqtt
+ description: Test broker
+ channels:
+ {channelKey}:
+ publish:
+ operationId: onMessageReceieved
+ message:
+ name: text
+ payload:
+ type: string
+ subscribe:
+ operationId: sendMessage
+ message:
+ name: text
+ payload:
+ type: string
+ """;
+
+ var document = new AsyncApiStringReader().Read(input, out var diagnostic);
+ diagnostic.Errors.First().Message.Should().Be($"The key '{channelKey}' in 'channels' MUST match the regular expression '^[a-zA-Z0-9\\.\\-_]+$'.");
+ diagnostic.Errors.First().Pointer.Should().Be("#/channels");
+ }
+
+ [Test]
+ public void ChannelKey_WithNonUniqueKey_DiagnosticsError()
+ {
+ var input =
+ """
+ asyncapi: 2.6.0
+ info:
+ title: Chat Application
+ version: 1.0.0
+ servers:
+ testing:
+ url: test.mosquitto.org:1883
+ protocol: mqtt
+ description: Test broker
+ channels:
+ chat/{personId}:
+ publish:
+ operationId: onMessageReceieved
+ message:
+ name: text
+ payload:
+ type: string
+ chat/{personIdentity}:
+ publish:
+ operationId: onMessageReceieved
+ message:
+ name: text
+ payload:
+ type: string
+ """;
+
+ var document = new AsyncApiStringReader().Read(input, out var diagnostic);
+ diagnostic.Errors.First().Message.Should().Be("Channel signature 'chat/{}' MUST be unique.");
+ diagnostic.Errors.First().Pointer.Should().Be("#/channels");
+ }
+
+ [Test]
+ [TestCase("chat")]
+ [TestCase("/some/chat/{personId}")]
+ [TestCase("chat-{personId}")]
+ [TestCase("chat-{person_id}")]
+ [TestCase("chat-{person%2Did}")]
+ [TestCase("chat-{personId2}")]
+ public void ChannelKey_WithValidKey_Success(string channelKey)
+ {
+ var input =
+ $"""
+ asyncapi: 2.6.0
+ info:
+ title: Chat Application
+ version: 1.0.0
+ servers:
+ testing:
+ url: test.mosquitto.org:1883
+ protocol: mqtt
+ description: Test broker
+ channels:
+ {channelKey}:
+ publish:
+ operationId: onMessageReceieved
+ message:
+ name: text
+ payload:
+ type: string
+ subscribe:
+ operationId: sendMessage
+ message:
+ name: text
+ payload:
+ type: string
+ """;
+
+ var document = new AsyncApiStringReader().Read(input, out var diagnostic);
+ diagnostic.Errors.Should().BeEmpty();
+ }
+ }
+
+}