diff --git a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs index d657a17..a8275f2 100644 --- a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs @@ -1,7 +1,12 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { + using System.Collections.Generic; + using System.Linq; + using ByteBard.AsyncAPI.Exceptions; using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.Interface; using ByteBard.AsyncAPI.Readers.ParseNodes; public class BindingDeserializer @@ -17,6 +22,52 @@ public static T LoadBinding(string nodeName, ParseNode node, FixedFieldMap return binding; } + internal static AsyncApiBindings LoadBindings( + MapNode mapNode, + IDictionary> parsers, + PatternFieldMap> patternFields) + where TBinding : IBinding + { + var context = mapNode.Context; + var bindings = new AsyncApiBindings(); + foreach (var property in mapNode) + { + if (patternFields.Keys.Any(isMatch => isMatch(property.Name))) + { + continue; + } + + if (!parsers.TryGetValue(property.Name, out var parser)) + { + // TBinding is the binding interface (e.g. IChannelBinding), so drop the leading "I" for the diagnostic. + var bindingType = typeof(TBinding).Name.Substring(1); + + // Reported as an error because the binding is dropped from the model and cannot be written back. + context.Diagnostic.Errors.Add( + new AsyncApiError(context.GetLocation(), $"{bindingType} {property.Name} is not found")); + continue; + } + + try + { + var binding = parser.LoadBinding(property); + if (binding != null) + { + bindings.Add(binding); + } + } + catch (AsyncApiException ex) + { + ex.Pointer = context.GetLocation(); + context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + } + + mapNode.ParseFields(bindings, patternFields); + + return bindings; + } + private static PatternFieldMap BindingPatternExtensionFields() where T : IBinding, new() { diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs index 3f6882c..805f99a 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs @@ -1,12 +1,18 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { - using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Readers.ParseNodes; internal static partial class AsyncApiV2Deserializer { + private static readonly PatternFieldMap> channelBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + internal static AsyncApiBindings LoadChannelBindings(ParseNode node) { var mapNode = node.CheckMapNode("channelBindings"); @@ -16,42 +22,7 @@ internal static AsyncApiBindings LoadChannelBindings(ParseNode return new AsyncApiBindingsReference(pointer); } - var channelBindings = new AsyncApiBindings(); - foreach (var property in mapNode) - { - var channelBinding = LoadChannelBinding(property); - - if (channelBinding != null) - { - channelBindings.Add(channelBinding); - } - else - { - mapNode.Context.Diagnostic.Errors.Add( - new AsyncApiError(node.Context.GetLocation(), $"ChannelBinding {property.Name} is not found")); - } - } - - return channelBindings; - } - - private static IChannelBinding LoadChannelBinding(ParseNode node) - { - var property = node as PropertyNode; - try - { - if (node.Context.ChannelBindingParsers.TryGetValue(property.Name, out var parser)) - { - return parser.LoadBinding(property); - } - } - catch (AsyncApiException ex) - { - ex.Pointer = node.Context.GetLocation(); - node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); - } - - return null; + return BindingDeserializer.LoadBindings(mapNode, node.Context.ChannelBindingParsers, channelBindingPatternFields); } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs index 692d621..1a53086 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs @@ -1,12 +1,18 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { - using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Readers.ParseNodes; internal static partial class AsyncApiV2Deserializer { + private static readonly PatternFieldMap> messageBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + internal static AsyncApiBindings LoadMessageBindings(ParseNode node) { var mapNode = node.CheckMapNode("messageBindings"); @@ -16,42 +22,7 @@ internal static AsyncApiBindings LoadMessageBindings(ParseNode return new AsyncApiBindingsReference(pointer); } - var messageBindings = new AsyncApiBindings(); - foreach (var property in mapNode) - { - var messageBinding = LoadMessageBinding(property); - - if (messageBinding != null) - { - messageBindings.Add(messageBinding); - } - else - { - mapNode.Context.Diagnostic.Errors.Add( - new AsyncApiError(node.Context.GetLocation(), $"MessageBinding {property.Name} is not found")); - } - } - - return messageBindings; - } - - internal static IMessageBinding LoadMessageBinding(ParseNode node) - { - var property = node as PropertyNode; - try - { - if (node.Context.MessageBindingParsers.TryGetValue(property.Name, out var parser)) - { - return parser.LoadBinding(property); - } - } - catch (AsyncApiException ex) - { - ex.Pointer = node.Context.GetLocation(); - node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); - } - - return null; + return BindingDeserializer.LoadBindings(mapNode, node.Context.MessageBindingParsers, messageBindingPatternFields); } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs index b01c212..f014d5d 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs @@ -1,12 +1,18 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { - using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Readers.ParseNodes; internal static partial class AsyncApiV2Deserializer { + private static readonly PatternFieldMap> operationBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + internal static AsyncApiBindings LoadOperationBindings(ParseNode node) { var mapNode = node.CheckMapNode("operationBindings"); @@ -16,42 +22,7 @@ internal static AsyncApiBindings LoadOperationBindings(ParseN return new AsyncApiBindingsReference(pointer); } - var operationBindings = new AsyncApiBindings(); - foreach (var property in mapNode) - { - var operationBinding = LoadOperationBinding(property); - - if (operationBinding != null) - { - operationBindings.Add(operationBinding); - } - else - { - mapNode.Context.Diagnostic.Errors.Add( - new AsyncApiError(node.Context.GetLocation(), $"OperationBinding {property.Name} is not found")); - } - } - - return operationBindings; - } - - internal static IOperationBinding LoadOperationBinding(ParseNode node) - { - var property = node as PropertyNode; - try - { - if (node.Context.OperationBindingParsers.TryGetValue(property.Name, out var parser)) - { - return parser.LoadBinding(property); - } - } - catch (AsyncApiException ex) - { - ex.Pointer = node.Context.GetLocation(); - node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); - } - - return null; + return BindingDeserializer.LoadBindings(mapNode, node.Context.OperationBindingParsers, operationBindingPatternFields); } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs index 88610dd..4b43447 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs @@ -1,12 +1,18 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { - using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Readers.ParseNodes; internal static partial class AsyncApiV2Deserializer { + private static readonly PatternFieldMap> serverBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + internal static AsyncApiBindings LoadServerBindings(ParseNode node) { var mapNode = node.CheckMapNode("serverBindings"); @@ -16,42 +22,7 @@ internal static AsyncApiBindings LoadServerBindings(ParseNode no return new AsyncApiBindingsReference(pointer); } - var serverBindings = new AsyncApiBindings(); - foreach (var property in mapNode) - { - var serverBinding = LoadServerBinding(property); - - if (serverBinding != null) - { - serverBindings.Add(serverBinding); - } - else - { - mapNode.Context.Diagnostic.Errors.Add( - new AsyncApiError(node.Context.GetLocation(), $"ServerBinding {property.Name} is not found")); - } - } - - return serverBindings; - } - - internal static IServerBinding LoadServerBinding(ParseNode node) - { - var property = node as PropertyNode; - try - { - if (node.Context.ServerBindingParsers.TryGetValue(property.Name, out var parser)) - { - return parser.LoadBinding(property); - } - } - catch (AsyncApiException ex) - { - ex.Pointer = node.Context.GetLocation(); - node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); - } - - return null; + return BindingDeserializer.LoadBindings(mapNode, node.Context.ServerBindingParsers, serverBindingPatternFields); } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs index e533fde..0133fee 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs @@ -1,6 +1,5 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { - using ByteBard.AsyncAPI.Exceptions; using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; @@ -8,6 +7,12 @@ internal static partial class AsyncApiV3Deserializer { + private static readonly PatternFieldMap> channelBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + internal static AsyncApiBindings LoadChannelBindings(ParseNode node) { var mapNode = node.CheckMapNode("channelBindings"); @@ -17,50 +22,7 @@ internal static AsyncApiBindings LoadChannelBindings(ParseNode return new AsyncApiBindingsReference(pointer); } - var channelBindings = new AsyncApiBindings(); - foreach (var property in mapNode) - { - var channelBinding = LoadChannelBinding(property); - - if (channelBinding != null) - { - channelBindings.Add(channelBinding); - } - else - { - mapNode.Context.Diagnostic.Errors.Add( - new AsyncApiError(node.Context.GetLocation(), $"ChannelBinding {property.Name} is not found")); - } - } - - mapNode.ParseFields(channelBindings, channelBindingPatternFields); - - return channelBindings; - } - - private static readonly PatternFieldMap> channelBindingPatternFields = - new() - { - { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, - }; - - private static IChannelBinding LoadChannelBinding(ParseNode node) - { - var property = node as PropertyNode; - try - { - if (node.Context.ChannelBindingParsers.TryGetValue(property.Name, out var parser)) - { - return parser.LoadBinding(property); - } - } - catch (AsyncApiException ex) - { - ex.Pointer = node.Context.GetLocation(); - node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); - } - - return null; + return BindingDeserializer.LoadBindings(mapNode, node.Context.ChannelBindingParsers, channelBindingPatternFields); } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs index 018e6db..5b6e8a0 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs @@ -1,6 +1,5 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { - using ByteBard.AsyncAPI.Exceptions; using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; @@ -8,6 +7,12 @@ internal static partial class AsyncApiV3Deserializer { + private static readonly PatternFieldMap> messageBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + internal static AsyncApiBindings LoadMessageBindings(ParseNode node) { var mapNode = node.CheckMapNode("messageBindings"); @@ -17,50 +22,7 @@ internal static AsyncApiBindings LoadMessageBindings(ParseNode return new AsyncApiBindingsReference(pointer); } - var messageBindings = new AsyncApiBindings(); - foreach (var property in mapNode) - { - var messageBinding = LoadMessageBinding(property); - - if (messageBinding != null) - { - messageBindings.Add(messageBinding); - } - else - { - mapNode.Context.Diagnostic.Errors.Add( - new AsyncApiError(node.Context.GetLocation(), $"MessageBinding {property.Name} is not found")); - } - } - - mapNode.ParseFields(messageBindings, messageBindingPatternFields); - - return messageBindings; - } - - private static readonly PatternFieldMap> messageBindingPatternFields = - new() - { - { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, - }; - - internal static IMessageBinding LoadMessageBinding(ParseNode node) - { - var property = node as PropertyNode; - try - { - if (node.Context.MessageBindingParsers.TryGetValue(property.Name, out var parser)) - { - return parser.LoadBinding(property); - } - } - catch (AsyncApiException ex) - { - ex.Pointer = node.Context.GetLocation(); - node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); - } - - return null; + return BindingDeserializer.LoadBindings(mapNode, node.Context.MessageBindingParsers, messageBindingPatternFields); } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs index 435c8e2..38086fb 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs @@ -1,6 +1,5 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { - using ByteBard.AsyncAPI.Exceptions; using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; @@ -8,6 +7,12 @@ internal static partial class AsyncApiV3Deserializer { + private static readonly PatternFieldMap> operationBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + internal static AsyncApiBindings LoadOperationBindings(ParseNode node) { var mapNode = node.CheckMapNode("operationBindings"); @@ -17,50 +22,7 @@ internal static AsyncApiBindings LoadOperationBindings(ParseN return new AsyncApiBindingsReference(pointer); } - var operationBindings = new AsyncApiBindings(); - foreach (var property in mapNode) - { - var operationBinding = LoadOperationBinding(property); - - if (operationBinding != null) - { - operationBindings.Add(operationBinding); - } - else - { - mapNode.Context.Diagnostic.Errors.Add( - new AsyncApiError(node.Context.GetLocation(), $"OperationBinding '{property.Name}' was not found")); - } - } - - mapNode.ParseFields(operationBindings, operationBindingPatternFields); - - return operationBindings; - } - - private static readonly PatternFieldMap> operationBindingPatternFields = - new() - { - { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, - }; - - internal static IOperationBinding LoadOperationBinding(ParseNode node) - { - var property = node as PropertyNode; - try - { - if (node.Context.OperationBindingParsers.TryGetValue(property.Name, out var parser)) - { - return parser.LoadBinding(property); - } - } - catch (AsyncApiException ex) - { - ex.Pointer = node.Context.GetLocation(); - node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); - } - - return null; + return BindingDeserializer.LoadBindings(mapNode, node.Context.OperationBindingParsers, operationBindingPatternFields); } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs index e410d4d..f38d4f9 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs @@ -1,65 +1,28 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { - using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Readers.ParseNodes; - using ByteBard.AsyncAPI.Extensions; internal static partial class AsyncApiV3Deserializer { - internal static AsyncApiBindings LoadServerBindings(ParseNode node) - { - var mapNode = node.CheckMapNode("serverBindings"); - var pointer = mapNode.GetReferencePointer(); - if (pointer != null) - { - return new AsyncApiBindingsReference(pointer); - } - - var serverBindings = new AsyncApiBindings(); - foreach (var property in mapNode) - { - var serverBinding = LoadServerBinding(property); - - if (serverBinding != null) - { - serverBindings.Add(serverBinding); - } - else - { - mapNode.Context.Diagnostic.Errors.Add( - new AsyncApiError(node.Context.GetLocation(), $"ServerBinding {property.Name} is not found")); - } - } - - mapNode.ParseFields(serverBindings, serverBindingPatternFields); - return serverBindings; - } - private static readonly PatternFieldMap> serverBindingPatternFields = new() { { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, }; - internal static IServerBinding LoadServerBinding(ParseNode node) + internal static AsyncApiBindings LoadServerBindings(ParseNode node) { - var property = node as PropertyNode; - try - { - if (node.Context.ServerBindingParsers.TryGetValue(property.Name, out var parser)) - { - return parser.LoadBinding(property); - } - } - catch (AsyncApiException ex) + var mapNode = node.CheckMapNode("serverBindings"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) { - ex.Pointer = node.Context.GetLocation(); - node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + return new AsyncApiBindingsReference(pointer); } - return null; + return BindingDeserializer.LoadBindings(mapNode, node.Context.ServerBindingParsers, serverBindingPatternFields); } } } diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/BindingsDiagnostics_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingsDiagnostics_Should.cs new file mode 100644 index 0000000..9927376 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingsDiagnostics_Should.cs @@ -0,0 +1,162 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings +{ + using System.Linq; + using ByteBard.AsyncAPI.Bindings; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers; + using FluentAssertions; + using NUnit.Framework; + + public class BindingsDiagnostics_Should : TestBase + { + private const string BindingsWithExtension = + """ + kafka: + bindingVersion: 0.4.0 + x-myextension: someValue + """; + + private const string BindingsWithUnknownBinding = + """ + kafka: + bindingVersion: 0.4.0 + unknown: + bindingVersion: 0.1.0 + """; + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBindings_WithExtension_ReadsExtensionWithoutDiagnostics(AsyncApiVersion version) + { + AssertExtensionIsRead(version); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ChannelBindings_WithExtension_ReadsExtensionWithoutDiagnostics(AsyncApiVersion version) + { + AssertExtensionIsRead(version); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void OperationBindings_WithExtension_ReadsExtensionWithoutDiagnostics(AsyncApiVersion version) + { + AssertExtensionIsRead(version); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void MessageBindings_WithExtension_ReadsExtensionWithoutDiagnostics(AsyncApiVersion version) + { + AssertExtensionIsRead(version); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBindings_WithUnregisteredBinding_AddsError(AsyncApiVersion version) + { + AssertUnregisteredBindingErrors(version, "ServerBinding"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ChannelBindings_WithUnregisteredBinding_AddsError(AsyncApiVersion version) + { + AssertUnregisteredBindingErrors(version, "ChannelBinding"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void OperationBindings_WithUnregisteredBinding_AddsError(AsyncApiVersion version) + { + AssertUnregisteredBindingErrors(version, "OperationBinding"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void MessageBindings_WithUnregisteredBinding_AddsError(AsyncApiVersion version) + { + AssertUnregisteredBindingErrors(version, "MessageBinding"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBindings_WithRegisteredAndUnregisteredBinding_ReserializesOnlyRegisteredBinding(AsyncApiVersion version) + { + AssertUnregisteredBindingIsNotReserialized(version); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ChannelBindings_WithRegisteredAndUnregisteredBinding_ReserializesOnlyRegisteredBinding(AsyncApiVersion version) + { + AssertUnregisteredBindingIsNotReserialized(version); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void OperationBindings_WithRegisteredAndUnregisteredBinding_ReserializesOnlyRegisteredBinding(AsyncApiVersion version) + { + AssertUnregisteredBindingIsNotReserialized(version); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void MessageBindings_WithRegisteredAndUnregisteredBinding_ReserializesOnlyRegisteredBinding(AsyncApiVersion version) + { + AssertUnregisteredBindingIsNotReserialized(version); + } + + private static AsyncApiBindings Read(string input, AsyncApiVersion version, out AsyncApiDiagnostic diagnostic) + where TBinding : IBinding + { + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Kafka; + return new AsyncApiStringReader(settings).ReadFragment>(input, version, out diagnostic); + } + + private static void AssertExtensionIsRead(AsyncApiVersion version) + where TBinding : IBinding + { + // Act + var bindings = Read(BindingsWithExtension, version, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + diagnostic.Warnings.Should().BeEmpty(); + bindings.Keys.Should().BeEquivalentTo("kafka"); + bindings.Extensions.Keys.Should().BeEquivalentTo("x-myextension"); + } + + private static void AssertUnregisteredBindingErrors(AsyncApiVersion version, string bindingType) + where TBinding : IBinding + { + // Act + var bindings = Read(BindingsWithUnknownBinding, version, out var diagnostic); + + // Assert + diagnostic.Errors.Select(e => e.Message).Should().BeEquivalentTo($"{bindingType} unknown is not found"); + diagnostic.Warnings.Should().BeEmpty(); + bindings.Keys.Should().BeEquivalentTo("kafka"); + } + + private static void AssertUnregisteredBindingIsNotReserialized(AsyncApiVersion version) + where TBinding : IBinding + { + // Arrange + var bindings = Read(BindingsWithUnknownBinding, version, out var diagnostic); + + // Act + var reserialized = bindings.SerializeAsYaml(version); + + // Assert + // The model has no way to hold a binding without a parser, so it is lost on write. + // That is why reading it must stay an error. + diagnostic.Errors.Should().ContainSingle(); + reserialized.Should().Contain("kafka"); + reserialized.Should().NotContain("unknown"); + } + } +}