From a7dee44a994126e23f28cb248a430335ebcf3fe2 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:45:30 +0100 Subject: [PATCH 1/4] fix: report unregistered bindings as warnings and stop flagging binding extensions Specification extensions (x-*) placed on a bindings object were reported as "Binding x-... is not found" errors, and in V2 they were dropped as well. Binding keys without a registered parser were reported as errors even though the document is valid. The eight V2/V3 binding loaders now share BindingDeserializer.LoadBindings, which: - skips extension keys and reads them as extensions (V2 and V3) - adds a warning instead of an error when no parser is registered for a key - no longer adds a second "not found" error when a parser fails Co-Authored-By: Claude Opus 5.5 --- .../BindingDeserializer.cs | 48 +++++++ .../V2/AsyncApiChannelBindingDeserializer.cs | 45 ++----- .../V2/AsyncApiMessageBindingDeserializer.cs | 45 ++----- .../AsyncApiOperationBindingDeserializer.cs | 45 ++----- .../V2/AsyncApiServerBindingDeserializer.cs | 45 ++----- .../V3/AsyncApiChannelBindingDeserializer.cs | 52 ++------ .../V3/AsyncApiMessageBindingDeserializer.cs | 52 ++------ .../AsyncApiOperationBindingDeserializer.cs | 52 ++------ .../V3/AsyncApiServerBindingDeserializer.cs | 51 ++------ .../Bindings/BindingsDiagnostics_Should.cs | 118 ++++++++++++++++++ 10 files changed, 226 insertions(+), 327 deletions(-) create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/BindingsDiagnostics_Should.cs diff --git a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs index d657a17..7dfa8c3 100644 --- a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs @@ -1,7 +1,12 @@ 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,49 @@ public static T LoadBinding(string nodeName, ParseNode node, FixedFieldMap return binding; } + internal static AsyncApiBindings LoadBindings( + string bindingType, + 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)) + { + context.Diagnostic.Warnings.Add( + new AsyncApiError(context.GetLocation(), $"{bindingType} '{property.Name}' was ignored because no binding parser is registered for it.")); + 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..59417a8 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 { - 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("ChannelBinding", 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..4087ac7 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 { - 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("MessageBinding", 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..f58aad0 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 { - 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("OperationBinding", 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..2bfa381 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 { - 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("ServerBinding", 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..f8b4f15 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 { - 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("ChannelBinding", 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..6fe91bb 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 { - 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("MessageBinding", 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..2768cab 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 { - 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("OperationBinding", 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..62e5e09 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 { - 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("ServerBinding", 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..37e61dd --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingsDiagnostics_Should.cs @@ -0,0 +1,118 @@ +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_AddsWarning(AsyncApiVersion version) + { + AssertUnregisteredBindingWarns(version, "ServerBinding"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ChannelBindings_WithUnregisteredBinding_AddsWarning(AsyncApiVersion version) + { + AssertUnregisteredBindingWarns(version, "ChannelBinding"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void OperationBindings_WithUnregisteredBinding_AddsWarning(AsyncApiVersion version) + { + AssertUnregisteredBindingWarns(version, "OperationBinding"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void MessageBindings_WithUnregisteredBinding_AddsWarning(AsyncApiVersion version) + { + AssertUnregisteredBindingWarns(version, "MessageBinding"); + } + + 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 AssertUnregisteredBindingWarns(AsyncApiVersion version, string bindingType) + where TBinding : IBinding + { + // Act + var bindings = Read(BindingsWithUnknownBinding, version, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + diagnostic.Warnings.Select(w => w.Message).Should().BeEquivalentTo( + $"{bindingType} 'unknown' was ignored because no binding parser is registered for it."); + bindings.Keys.Should().BeEquivalentTo("kafka"); + } + } +} From 8772aa9d3ce482b86801d3f1db4d1dddd3650b62 Mon Sep 17 00:00:00 2001 From: Arkadiusz biel Date: Tue, 13 Oct 2026 08:14:26 +0100 Subject: [PATCH 2/4] fix: keep unregistered bindings as errors A binding without a registered parser is dropped from the model and is lost when the document is written back, so reading it stays an error with its original message. Adds tests covering a registered and an unregistered binding read together and re-serialized. Co-Authored-By: Claude Opus 5.5 --- .../BindingDeserializer.cs | 5 +- .../Bindings/BindingsDiagnostics_Should.cs | 68 +++++++++++++++---- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs index 7dfa8c3..e9980aa 100644 --- a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs @@ -40,8 +40,9 @@ internal static AsyncApiBindings LoadBindings( if (!parsers.TryGetValue(property.Name, out var parser)) { - context.Diagnostic.Warnings.Add( - new AsyncApiError(context.GetLocation(), $"{bindingType} '{property.Name}' was ignored because no binding parser is registered for it.")); + // 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; } diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/BindingsDiagnostics_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingsDiagnostics_Should.cs index 37e61dd..9927376 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/BindingsDiagnostics_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingsDiagnostics_Should.cs @@ -55,30 +55,58 @@ public void MessageBindings_WithExtension_ReadsExtensionWithoutDiagnostics(Async [TestCase(AsyncApiVersion.AsyncApi2_0)] [TestCase(AsyncApiVersion.AsyncApi3_0)] - public void ServerBindings_WithUnregisteredBinding_AddsWarning(AsyncApiVersion version) + public void ServerBindings_WithUnregisteredBinding_AddsError(AsyncApiVersion version) { - AssertUnregisteredBindingWarns(version, "ServerBinding"); + AssertUnregisteredBindingErrors(version, "ServerBinding"); } [TestCase(AsyncApiVersion.AsyncApi2_0)] [TestCase(AsyncApiVersion.AsyncApi3_0)] - public void ChannelBindings_WithUnregisteredBinding_AddsWarning(AsyncApiVersion version) + public void ChannelBindings_WithUnregisteredBinding_AddsError(AsyncApiVersion version) { - AssertUnregisteredBindingWarns(version, "ChannelBinding"); + AssertUnregisteredBindingErrors(version, "ChannelBinding"); } [TestCase(AsyncApiVersion.AsyncApi2_0)] [TestCase(AsyncApiVersion.AsyncApi3_0)] - public void OperationBindings_WithUnregisteredBinding_AddsWarning(AsyncApiVersion version) + public void OperationBindings_WithUnregisteredBinding_AddsError(AsyncApiVersion version) { - AssertUnregisteredBindingWarns(version, "OperationBinding"); + AssertUnregisteredBindingErrors(version, "OperationBinding"); } [TestCase(AsyncApiVersion.AsyncApi2_0)] [TestCase(AsyncApiVersion.AsyncApi3_0)] - public void MessageBindings_WithUnregisteredBinding_AddsWarning(AsyncApiVersion version) + public void MessageBindings_WithUnregisteredBinding_AddsError(AsyncApiVersion version) { - AssertUnregisteredBindingWarns(version, "MessageBinding"); + 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) @@ -102,17 +130,33 @@ private static void AssertExtensionIsRead(AsyncApiVersion version) bindings.Extensions.Keys.Should().BeEquivalentTo("x-myextension"); } - private static void AssertUnregisteredBindingWarns(AsyncApiVersion version, string bindingType) + private static void AssertUnregisteredBindingErrors(AsyncApiVersion version, string bindingType) where TBinding : IBinding { // Act var bindings = Read(BindingsWithUnknownBinding, version, out var diagnostic); // Assert - diagnostic.Errors.Should().BeEmpty(); - diagnostic.Warnings.Select(w => w.Message).Should().BeEquivalentTo( - $"{bindingType} 'unknown' was ignored because no binding parser is registered for it."); + 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"); + } } } From bd679d900c3cb7a361b12382a8857009703f87d0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:53:42 +0100 Subject: [PATCH 3/4] refactor: infer binding type name from TBinding and drop BOMs Address review feedback on the shared binding loader: - LoadBindings no longer takes the binding type name as a parameter; it is derived from the TBinding interface name for the diagnostic message. - Remove the UTF-8 byte-order mark from the files touched by this change. Co-Authored-By: Claude Opus 5.5 --- src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs | 6 ++++-- .../V2/AsyncApiChannelBindingDeserializer.cs | 4 ++-- .../V2/AsyncApiMessageBindingDeserializer.cs | 4 ++-- .../V2/AsyncApiOperationBindingDeserializer.cs | 4 ++-- .../V2/AsyncApiServerBindingDeserializer.cs | 4 ++-- .../V3/AsyncApiChannelBindingDeserializer.cs | 4 ++-- .../V3/AsyncApiMessageBindingDeserializer.cs | 4 ++-- .../V3/AsyncApiOperationBindingDeserializer.cs | 4 ++-- .../V3/AsyncApiServerBindingDeserializer.cs | 4 ++-- 9 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs index e9980aa..2d148c3 100644 --- a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { using System.Collections.Generic; using System.Linq; @@ -23,13 +23,15 @@ public static T LoadBinding(string nodeName, ParseNode node, FixedFieldMap } internal static AsyncApiBindings LoadBindings( - string bindingType, MapNode mapNode, IDictionary> parsers, PatternFieldMap> patternFields) where TBinding : IBinding { var context = mapNode.Context; + + // TBinding is the binding interface (e.g. IChannelBinding), so drop the leading "I" for the diagnostic. + var bindingType = typeof(TBinding).Name.Substring(1); var bindings = new AsyncApiBindings(); foreach (var property in mapNode) { diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs index 59417a8..805f99a 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; @@ -22,7 +22,7 @@ internal static AsyncApiBindings LoadChannelBindings(ParseNode return new AsyncApiBindingsReference(pointer); } - return BindingDeserializer.LoadBindings("ChannelBinding", mapNode, node.Context.ChannelBindingParsers, channelBindingPatternFields); + 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 4087ac7..1a53086 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; @@ -22,7 +22,7 @@ internal static AsyncApiBindings LoadMessageBindings(ParseNode return new AsyncApiBindingsReference(pointer); } - return BindingDeserializer.LoadBindings("MessageBinding", mapNode, node.Context.MessageBindingParsers, messageBindingPatternFields); + 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 f58aad0..f014d5d 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; @@ -22,7 +22,7 @@ internal static AsyncApiBindings LoadOperationBindings(ParseN return new AsyncApiBindingsReference(pointer); } - return BindingDeserializer.LoadBindings("OperationBinding", mapNode, node.Context.OperationBindingParsers, operationBindingPatternFields); + 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 2bfa381..4b43447 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; @@ -22,7 +22,7 @@ internal static AsyncApiBindings LoadServerBindings(ParseNode no return new AsyncApiBindingsReference(pointer); } - return BindingDeserializer.LoadBindings("ServerBinding", mapNode, node.Context.ServerBindingParsers, serverBindingPatternFields); + 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 f8b4f15..0133fee 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; @@ -22,7 +22,7 @@ internal static AsyncApiBindings LoadChannelBindings(ParseNode return new AsyncApiBindingsReference(pointer); } - return BindingDeserializer.LoadBindings("ChannelBinding", mapNode, node.Context.ChannelBindingParsers, channelBindingPatternFields); + 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 6fe91bb..5b6e8a0 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; @@ -22,7 +22,7 @@ internal static AsyncApiBindings LoadMessageBindings(ParseNode return new AsyncApiBindingsReference(pointer); } - return BindingDeserializer.LoadBindings("MessageBinding", mapNode, node.Context.MessageBindingParsers, messageBindingPatternFields); + 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 2768cab..38086fb 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; @@ -22,7 +22,7 @@ internal static AsyncApiBindings LoadOperationBindings(ParseN return new AsyncApiBindingsReference(pointer); } - return BindingDeserializer.LoadBindings("OperationBinding", mapNode, node.Context.OperationBindingParsers, operationBindingPatternFields); + 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 62e5e09..f38d4f9 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; @@ -22,7 +22,7 @@ internal static AsyncApiBindings LoadServerBindings(ParseNode no return new AsyncApiBindingsReference(pointer); } - return BindingDeserializer.LoadBindings("ServerBinding", mapNode, node.Context.ServerBindingParsers, serverBindingPatternFields); + return BindingDeserializer.LoadBindings(mapNode, node.Context.ServerBindingParsers, serverBindingPatternFields); } } } From e8dbf666e05a5306de33c68a2507a3e5f0e300c8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:07:48 +0100 Subject: [PATCH 4/4] refactor: resolve binding type name only when reporting the error Move the typeof(TBinding) lookup into the unregistered-binding branch so it is not evaluated for every bindings object. Co-Authored-By: Claude Opus 5.5 --- src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs index 2d148c3..a8275f2 100644 --- a/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs @@ -29,9 +29,6 @@ internal static AsyncApiBindings LoadBindings( where TBinding : IBinding { var context = mapNode.Context; - - // TBinding is the binding interface (e.g. IChannelBinding), so drop the leading "I" for the diagnostic. - var bindingType = typeof(TBinding).Name.Substring(1); var bindings = new AsyncApiBindings(); foreach (var property in mapNode) { @@ -42,6 +39,9 @@ internal static AsyncApiBindings LoadBindings( 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"));