From 39ff82c2e947a50a12f08b44e129abd7898607c4 Mon Sep 17 00:00:00 2001 From: Yohan Fraga Date: Fri, 7 Mar 2025 17:42:03 -0300 Subject: [PATCH 1/3] YFS - Start tests to v3 validation rules --- .../V3/AsyncApiV3VersionService.cs | 2 +- .../Validation/AsyncApiValidator.cs | 6 ++ .../Rules/AsyncApiOperationRules.cs | 14 +++- .../Validation/ValidationRuleTests.cs | 76 +++++++++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/LEGO.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs b/src/LEGO.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs index dd0122d..8eba7bd 100644 --- a/src/LEGO.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs +++ b/src/LEGO.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs @@ -79,7 +79,7 @@ public AsyncApiReference ConvertToAsyncApiReference( public AsyncApiDocument LoadDocument(RootNode rootNode) { - return AsyncApiV2Deserializer.LoadAsyncApi(rootNode); + return AsyncApiV3Deserializer.LoadAsyncApi(rootNode); } public T LoadElement(ParseNode node) diff --git a/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs b/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs index 1b9bfc0..64a8e1d 100644 --- a/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs +++ b/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs @@ -143,6 +143,12 @@ public void AddWarning(AsyncApiValidatorWarning warning) /// The object to be validated. public override void Visit(AsyncApiServer item) => this.Validate(item); + /// + /// Execute validation rules against an . + /// + /// The object to be validated. + public override void Visit(AsyncApiOperation item) => this.Validate(item); + public override void Visit(IServerBinding item) => this.Validate(item); public override void Visit(IChannelBinding item) => this.Validate(item); diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs index 61607d7..a4dd105 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs @@ -46,9 +46,9 @@ public static class AsyncApiOperationRules } var channels = - context.RootDocument.Channels.Values.Where(channel => channel.Equals(operation.Channel)); + context.RootDocument.Channels.Values.Where(channel => operation.Channel.Equals(channel)); - var referencedChannel = channels.FirstOrDefault(c => c.Equals(operation.Channel)); + var referencedChannel = channels.FirstOrDefault(c => operation.Channel.Equals(c)); if (referencedChannel == null) { context.CreateError( @@ -56,7 +56,9 @@ public static class AsyncApiOperationRules string.Format(Resource.Validation_OperationMustReferenceValidChannel, operation.Title)); return; } - if (!operation.Messages.All(refMessage => referencedChannel.Messages.Any(message => message.Equals(refMessage)))) + + // TODO: check this validation + if (!operation.Messages.All(refMessage => referencedChannel.Messages.Any(message => refMessage.Equals(message)))) { context.CreateError( "OperationChannelRef", @@ -73,6 +75,12 @@ public static class AsyncApiOperationRules context.RootDocument.Channels.Values.Where(channel => channel.Equals(operation.Channel)); var referencedChannel = channels.FirstOrDefault(c => c.Equals(operation.Channel)); + + if (referencedChannel == null) + { + return; + } + if (!operation.Messages.All(refMessage => referencedChannel.Messages.Any(message => message.Equals(refMessage)))) { context.CreateError( diff --git a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs index 407858c..92adcba 100644 --- a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs @@ -45,6 +45,82 @@ public void V2_OperationId_WithNonUniqueKey_DiagnosticsError() diagnostic.Errors.First().Pointer.Should().Be("#/channels/chat~1{personIdentity}"); } + [Test] + public void V3_OperationChannel_NotReferencingARootChannel_DiagnosticsError() + { + var input = + """ + asyncapi: 3.0.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + host: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + chatPersonId: + address: chat.{personId} + messages: + messageReceived: + name: text + payload: + type: string + operations: + onMessageReceived: + title: Message received + channel: + $ref: '#/components/channels/secondChannel' + messages: + - $ref: '#/channels/chatPersonId/messages/messageReceived' + components: + channels: + secondChannel: + address: chat.{secondChannel} + """; + + var document = new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be("The operation 'Message received' MUST point to a channel definition located in the root Channels Object."); + diagnostic.Errors.First().Pointer.Should().Be("#/operations/onMessageReceived"); + } + + [Test] + public void V3_OperationMessage_NotReferencingARootChannel_DiagnosticsError() + { + var input = + """ + asyncapi: 3.0.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + host: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + chatPersonId: + address: chat.{personId} + messages: + messageReceived: + name: text + payload: + type: string + operations: + onMessageReceived: + title: Message received + channel: + $ref: '#/channels/chatPersonId' + messages: + - $ref: '#/channels/chatPersonId/messages/messageReceived' + """; + + var document = new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be("The operation 'Message received' MUST point to a channel definition located in the root Channels Object."); + diagnostic.Errors.First().Pointer.Should().Be("#/operations/onMessageReceived"); + } + [Test] [TestCase("chat")] [TestCase("/some/chat/{personId}")] From a9d8c3f91d74ca63c8877ce97c5dd5f8d591bf3d Mon Sep 17 00:00:00 2001 From: Yohan Fraga Date: Thu, 13 Mar 2025 08:39:45 -0300 Subject: [PATCH 2/3] YFS - Add operations messages test --- .../References/AsyncApiMessageReference.cs | 5 + .../Rules/AsyncApiOperationRules.cs | 26 +- .../Writers/AsyncApiWriterSettings.cs | 2 +- .../Validation/ValidationRuleTests.cs | 312 +++++++++--------- 4 files changed, 176 insertions(+), 169 deletions(-) diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageReference.cs index b0801fd..414f54a 100644 --- a/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageReference.cs +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageReference.cs @@ -71,6 +71,11 @@ public AsyncApiMessageReference(string reference) public bool Equals(AsyncApiMessageReference other) { + if (other.Target is AsyncApiMessageReference reference) + { + return this.Equals(reference); + } + return this.Target == other.Target; } diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs index a4dd105..f800fb9 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs @@ -1,5 +1,6 @@ // Copyright (c) The LEGO Group. All rights reserved. +using System.Collections.Generic; using System.Linq; namespace LEGO.AsyncAPI.Validation.Rules @@ -54,16 +55,6 @@ public static class AsyncApiOperationRules context.CreateError( "OperationChannelRef", string.Format(Resource.Validation_OperationMustReferenceValidChannel, operation.Title)); - return; - } - - // TODO: check this validation - if (!operation.Messages.All(refMessage => referencedChannel.Messages.Any(message => refMessage.Equals(message)))) - { - context.CreateError( - "OperationChannelRef", - string.Format(Resource.Validation_OperationMessagesMustReferenceOperationChannel, operation.Title)); - return; } }); @@ -72,22 +63,29 @@ public static class AsyncApiOperationRules (context, operation) => { var channels = - context.RootDocument.Channels.Values.Where(channel => channel.Equals(operation.Channel)); + context.RootDocument.Channels.Values.Where(channel => operation.Channel.Equals(channel)); - var referencedChannel = channels.FirstOrDefault(c => c.Equals(operation.Channel)); + var referencedChannel = channels.FirstOrDefault(c => operation.Channel.Equals(c)); if (referencedChannel == null) { return; } - if (!operation.Messages.All(refMessage => referencedChannel.Messages.Any(message => message.Equals(refMessage)))) + if (!AllOperationsMessagesReferencesChannelMessages(operation.Messages, referencedChannel.Messages.Values)) { context.CreateError( "OperationChannelRef", string.Format(Resource.Validation_OperationMessagesMustReferenceOperationChannel, operation.Title)); - return; } }); + + private static bool AllOperationsMessagesReferencesChannelMessages( + IList operationMessages, ICollection channelMessages) => + operationMessages.All(opMessage => OperationMessageReferencesAnyChannelMessage(opMessage, channelMessages)); + + private static bool OperationMessageReferencesAnyChannelMessage( + AsyncApiMessageReference operationMessage, ICollection channelMessages) => + channelMessages.Any(channelMessage => channelMessage.Equals(operationMessage)); } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs index 85d3607..45de537 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs @@ -69,7 +69,7 @@ public ReferenceInlineSetting ReferenceInline /// /// Returns back if the refernece should be inlined or not. /// - /// The refernece. + /// The reference. /// True if it should be inlined otherwise false. public bool ShouldInlineReference(AsyncApiReference reference) { diff --git a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs index 92adcba..ceb9484 100644 --- a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs @@ -1,165 +1,169 @@ // Copyright (c) The LEGO Group. All rights reserved. -namespace LEGO.AsyncAPI.Tests.Validation -{ - using FluentAssertions; - using LEGO.AsyncAPI.Readers; - using NUnit.Framework; - using System.Linq; +namespace LEGO.AsyncAPI.Tests.Validation; - public class ValidationRuleTests - { - [Test] - public void V2_OperationId_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 - """; +using System.Linq; +using FluentAssertions; +using LEGO.AsyncAPI.Readers; +using NUnit.Framework; - var document = new AsyncApiStringReader().Read(input, out var diagnostic); - diagnostic.Errors.First().Message.Should().Be("OperationId: 'onMessageReceieved' is not unique."); - diagnostic.Errors.First().Pointer.Should().Be("#/channels/chat~1{personIdentity}"); - } +public class ValidationRuleTests +{ + [Test] + public void V2_OperationId_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 + """; - [Test] - public void V3_OperationChannel_NotReferencingARootChannel_DiagnosticsError() - { - var input = - """ - asyncapi: 3.0.0 - info: - title: Chat Application - version: 1.0.0 - servers: - testing: - host: test.mosquitto.org:1883 - protocol: mqtt - description: Test broker - channels: - chatPersonId: - address: chat.{personId} - messages: - messageReceived: - name: text - payload: - type: string - operations: - onMessageReceived: - title: Message received - channel: - $ref: '#/components/channels/secondChannel' - messages: - - $ref: '#/channels/chatPersonId/messages/messageReceived' - components: - channels: - secondChannel: - address: chat.{secondChannel} - """; + new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be("OperationId: 'onMessageReceieved' is not unique."); + diagnostic.Errors.First().Pointer.Should().Be("#/channels/chat~1{personIdentity}"); + } - var document = new AsyncApiStringReader().Read(input, out var diagnostic); - diagnostic.Errors.First().Message.Should().Be("The operation 'Message received' MUST point to a channel definition located in the root Channels Object."); - diagnostic.Errors.First().Pointer.Should().Be("#/operations/onMessageReceived"); - } + [Test] + public void V3_OperationChannel_NotReferencingARootChannel_DiagnosticsError() + { + var input = + """ + asyncapi: 3.0.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + host: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + chatPersonId: + address: chat.{personId} + messages: + messageReceived: + name: text + payload: + type: string + operations: + onMessageReceived: + title: Message received + channel: + $ref: '#/components/channels/secondChannel' + messages: + - $ref: '#/channels/chatPersonId/messages/messageReceived' + components: + channels: + secondChannel: + address: chat.{secondChannel} + """; - [Test] - public void V3_OperationMessage_NotReferencingARootChannel_DiagnosticsError() - { - var input = - """ - asyncapi: 3.0.0 - info: - title: Chat Application - version: 1.0.0 - servers: - testing: - host: test.mosquitto.org:1883 - protocol: mqtt - description: Test broker - channels: - chatPersonId: - address: chat.{personId} - messages: - messageReceived: - name: text - payload: - type: string - operations: - onMessageReceived: - title: Message received - channel: - $ref: '#/channels/chatPersonId' - messages: - - $ref: '#/channels/chatPersonId/messages/messageReceived' - """; + new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be("The operation 'Message received' MUST point to a channel definition located in the root Channels Object."); + diagnostic.Errors.First().Pointer.Should().Be("#/operations/onMessageReceived"); + } - var document = new AsyncApiStringReader().Read(input, out var diagnostic); - diagnostic.Errors.First().Message.Should().Be("The operation 'Message received' MUST point to a channel definition located in the root Channels Object."); - diagnostic.Errors.First().Pointer.Should().Be("#/operations/onMessageReceived"); - } + [Test] + public void V3_OperationMessage_NotReferencingARootChannel_DiagnosticsError() + { + var input = + """ + asyncapi: 3.0.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + host: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + chatPersonId: + address: chat.{personId} + messages: + messageReceived: + name: text + payload: + type: string + operations: + onMessageReceived: + title: Message received + channel: + $ref: '#/channels/chatPersonId' + messages: + - $ref: '#/components/messages/messageSent' + components: + messages: + messageSent: + name: text + payload: + type: string + """; - [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 - """; + new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be("The messages of operation 'Message received' MUST be a subset of the referenced channels messages."); + diagnostic.Errors.First().Pointer.Should().Be("#/operations/onMessageReceived"); + } - var document = new AsyncApiStringReader().Read(input, out var diagnostic); - diagnostic.Errors.Should().BeEmpty(); - } - } + [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 + """; -} + new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.Should().BeEmpty(); + } +} \ No newline at end of file From 2b93cf15a36c23cf65bf8b9a9e9e0927bd8022e2 Mon Sep 17 00:00:00 2001 From: Yohan Fraga Date: Thu, 13 Mar 2025 09:19:10 -0300 Subject: [PATCH 3/3] YFS - Fix context RootDocument null check --- src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs index f800fb9..b229d15 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs @@ -62,6 +62,11 @@ public static class AsyncApiOperationRules new ValidationRule( (context, operation) => { + if (context.RootDocument?.Operations.Values.FirstOrDefault(op => op == operation) is null) + { + return; + } + var channels = context.RootDocument.Channels.Values.Where(channel => operation.Channel.Equals(channel));