From 2666c7fc9735c2637c648d55eadce2060f6fe5bf Mon Sep 17 00:00:00 2001 From: VisualBean Date: Thu, 23 Jan 2025 12:42:13 +0100 Subject: [PATCH 1/4] add validation rule and tests --- .../Validation/Rules/AsyncApiDocumentRules.cs | 33 +++++-- .../Validation/ValidationRuleTests.cs | 89 +++++++++++++++++++ .../Validation/ValidationRulesetTests.cs | 3 + 3 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs index 27076369..a5c4f0b8 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs @@ -2,6 +2,7 @@ namespace LEGO.AsyncAPI.Validation.Rules { + using System; using System.Linq; using System.Text.RegularExpressions; using LEGO.AsyncAPI.Models; @@ -10,10 +11,12 @@ 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,14 +33,30 @@ 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")); - } + if (document.Channels == null || !document.Channels.Keys.Any()) + { + context.CreateError( + nameof(DocumentRequiredFields), + string.Format(Resource.Validation_FieldRequired, "channels", "document")); + return; + } - context.Exit(); + foreach (var key in document.Channels.Keys) + { + if (!ChannelKeyUriTemplateRegex.IsMatch(key)) + { + context.CreateError( + "ChannelKeys", + string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "channels", KeyRegex.ToString())); + } + } + } + finally + { + context.Exit(); + } }); public static ValidationRule KeyMustBeRegularExpression => diff --git a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs new file mode 100644 index 00000000..f2ad7729 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs @@ -0,0 +1,89 @@ +// 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] + [TestCase("chat")] + [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(); + } + } + +} diff --git a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs index 6dda6f63..37e94375 100644 --- a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs @@ -2,8 +2,11 @@ namespace LEGO.AsyncAPI.Tests.Validation { + using FluentAssertions; + using LEGO.AsyncAPI.Readers; using LEGO.AsyncAPI.Validations; using NUnit.Framework; + using System.Linq; public class ValidationRuleSetTests { From 5822895923eda57d98d9e26886fd3bc18647b86d Mon Sep 17 00:00:00 2001 From: VisualBean Date: Thu, 23 Jan 2025 12:44:41 +0100 Subject: [PATCH 2/4] additional test for `/` --- test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs index f2ad7729..78e6028e 100644 --- a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs @@ -48,6 +48,7 @@ public void ChannelKey_WithInvalidParameter_DiagnosticsError(string channelKey) [Test] [TestCase("chat")] + [TestCase("/some/chat/{personId}")] [TestCase("chat-{personId}")] [TestCase("chat-{person_id}")] [TestCase("chat-{person%2Did}")] From 1b1498cbed8415628f50f3dfaef15dd73d437095 Mon Sep 17 00:00:00 2001 From: VisualBean Date: Thu, 23 Jan 2025 13:10:31 +0100 Subject: [PATCH 3/4] add uniqueness check as well --- src/LEGO.AsyncAPI/Resource.Designer.cs | 9 +++++ src/LEGO.AsyncAPI/Resource.resx | 3 ++ .../Validation/Rules/AsyncApiDocumentRules.cs | 30 +++++++++++++++- .../Validation/ValidationRuleTests.cs | 36 +++++++++++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) 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 a5c4f0b8..d866c4df 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs @@ -3,6 +3,7 @@ namespace LEGO.AsyncAPI.Validation.Rules { using System; + using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using LEGO.AsyncAPI.Models; @@ -12,6 +13,7 @@ namespace LEGO.AsyncAPI.Validation.Rules public static class AsyncApiDocumentRules { private static TimeSpan RegexTimeout = TimeSpan.FromSeconds(1); + /// /// The key regex. /// @@ -35,6 +37,7 @@ public static class AsyncApiDocumentRules context.Enter("channels"); try { + // MUST have at least 1 channel if (document.Channels == null || !document.Channels.Keys.Any()) { context.CreateError( @@ -42,15 +45,23 @@ public static class AsyncApiDocumentRules 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())); } + + // Unique channel keys + var pathSignature = GetKeySignature(key); + if (!hashSet.Add(pathSignature)) + { + context.CreateError("ChannelKey", string.Format(Resource.Validation_ChannelsMustBeUnique, pathSignature)); + } } } finally @@ -59,6 +70,23 @@ public static class AsyncApiDocumentRules } }); + 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 index 78e6028e..5d9f7bec 100644 --- a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs @@ -46,6 +46,42 @@ public void ChannelKey_WithInvalidParameter_DiagnosticsError(string channelKey) 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}")] From 864c638db6331bda374106cab551b0dce0612b75 Mon Sep 17 00:00:00 2001 From: VisualBean Date: Thu, 23 Jan 2025 13:11:44 +0100 Subject: [PATCH 4/4] remove usings --- test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs index 37e94375..6dda6f63 100644 --- a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs @@ -2,11 +2,8 @@ namespace LEGO.AsyncAPI.Tests.Validation { - using FluentAssertions; - using LEGO.AsyncAPI.Readers; using LEGO.AsyncAPI.Validations; using NUnit.Framework; - using System.Linq; public class ValidationRuleSetTests {