Skip to content

Commit 8b93442

Browse files
committed
fix: read a binding registered under an x- key as that binding
A custom binding may already use an x- key (BindingKey => "x-signalr") so its documents validate with any reader. Such a key matched the extension pattern first and was read as a generic extension. Look up the registered parsers before treating an x- key as an extension, in both versions.
1 parent 60e5ce6 commit 8b93442

3 files changed

Lines changed: 70 additions & 10 deletions

File tree

‎src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,20 @@ internal static AsyncApiBindings<TBinding> LoadBindings<TBinding>(
3838
var bindingType = typeof(TBinding).Name.Substring(1);
3939
foreach (var property in mapNode)
4040
{
41+
// A registered key is read as that binding, even when it carries the x- prefix itself.
4142
var key = property.Name;
42-
if (version == AsyncApiVersion.AsyncApi3_0 && TryGetCustomBindingKey(property.Name, parsers, out var customKey))
43+
if (!parsers.ContainsKey(key))
4344
{
44-
// AsyncAPI 3 writes a custom binding as an extension (x-<key>); read it back as the binding.
45-
key = customKey;
46-
}
47-
else if (patternFields.Keys.Any(isMatch => isMatch(property.Name)))
48-
{
49-
property.ParseField(bindings, patternFields);
50-
continue;
45+
if (version == AsyncApiVersion.AsyncApi3_0 && TryGetCustomBindingKey(key, parsers, out var customKey))
46+
{
47+
// AsyncAPI 3 writes a custom binding as an extension (x-<key>); read it back as the binding.
48+
key = customKey;
49+
}
50+
else if (patternFields.Keys.Any(isMatch => isMatch(key)))
51+
{
52+
property.ParseField(bindings, patternFields);
53+
continue;
54+
}
5155
}
5256

5357
if (!parsers.TryGetValue(key, out var parser))
@@ -58,7 +62,7 @@ internal static AsyncApiBindings<TBinding> LoadBindings<TBinding>(
5862
continue;
5963
}
6064

61-
if (version == AsyncApiVersion.AsyncApi3_0 && key == property.Name && !AsyncApiBindingKeys.IsSpecificationKey(key))
65+
if (version == AsyncApiVersion.AsyncApi3_0 && AsyncApiBindingKeys.ToV3Key(key) != property.Name)
6266
{
6367
// AsyncAPI 3 bindings objects only allow the specification's binding names and extensions. The binding
6468
// is still read, and written back as x-<key>.

‎test/ByteBard.AsyncAPI.Tests/Bindings/CustomBindingKey_Should.cs‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using ByteBard.AsyncAPI.Models.Interfaces;
88
using ByteBard.AsyncAPI.Readers;
99
using ByteBard.AsyncAPI.Readers.Interface;
10+
using ByteBard.AsyncAPI.Readers.ParseNodes;
11+
using ByteBard.AsyncAPI.Writers;
1012
using FluentAssertions;
1113
using NUnit.Framework;
1214

@@ -147,6 +149,28 @@ public void V3_ExtensionWithoutRegisteredBinding_ReadsAsExtension()
147149
channel.Bindings.Extensions.Keys.Should().BeEquivalentTo("x-other");
148150
}
149151

152+
[TestCase(AsyncApiVersion.AsyncApi2_0)]
153+
[TestCase(AsyncApiVersion.AsyncApi3_0)]
154+
public void BindingKeyWithExtensionPrefix_ReadsAndWritesAsBinding(AsyncApiVersion version)
155+
{
156+
// Arrange
157+
var input =
158+
"""
159+
bindings:
160+
x-prefixed:
161+
custom: someValue
162+
""";
163+
164+
// Act
165+
var channel = ReadChannel(input, version, out var diagnostic);
166+
167+
// Assert
168+
diagnostic.Errors.Should().BeEmpty();
169+
channel.Bindings.Extensions.Should().BeEmpty();
170+
((PrefixedBinding)channel.Bindings["x-prefixed"]).Custom.Should().Be("someValue");
171+
channel.SerializeAsYaml(version).Should().BePlatformAgnosticEquivalentTo(input);
172+
}
173+
150174
[TestCase("kafka", true)]
151175
[TestCase("ws", true)]
152176
[TestCase("ros2", true)]
@@ -172,7 +196,39 @@ private static AsyncApiChannel ReadChannel(string input, AsyncApiVersion version
172196
var settings = new AsyncApiReaderSettings();
173197
settings.Bindings = BindingsCollection.Kafka;
174198
settings.Bindings.Add(new MyBinding());
199+
settings.Bindings.Add(new PrefixedBinding());
175200
return new AsyncApiStringReader(settings).ReadFragment<AsyncApiChannel>(input, version, out diagnostic);
176201
}
177202
}
203+
204+
/// <summary>A custom binding whose key already carries the extension prefix.</summary>
205+
public class PrefixedBinding : ChannelBinding<PrefixedBinding>
206+
{
207+
public string Custom { get; set; }
208+
209+
public override string BindingKey => "x-prefixed";
210+
211+
protected override FixedFieldMap<PrefixedBinding> FixedFieldMap => new FixedFieldMap<PrefixedBinding>()
212+
{
213+
{ "custom", (a, n) => { a.Custom = n.GetScalarValue(); } },
214+
};
215+
216+
public override void SerializeV2(IAsyncApiWriter writer)
217+
{
218+
this.SerializeProperties(writer);
219+
}
220+
221+
public override void SerializeV3(IAsyncApiWriter writer)
222+
{
223+
this.SerializeProperties(writer);
224+
}
225+
226+
public override void SerializeProperties(IAsyncApiWriter writer)
227+
{
228+
writer.WriteStartObject();
229+
writer.WriteRequiredProperty("custom", this.Custom);
230+
writer.WriteExtensions(this.Extensions);
231+
writer.WriteEndObject();
232+
}
233+
}
178234
}

‎website/docs/guides/custom-bindings.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ var document = new AsyncApiStringReader(settings).Read(input, out var diagnostic
5151

5252
## Binding keys in AsyncAPI 3
5353

54-
AsyncAPI 3 bindings objects only allow the binding names the specification lists (`amqp`, `http`, `kafka`, `ws` and so on) and `x-` extensions. When a document is serialized as AsyncAPI 3, a binding whose `BindingKey` is not one of those names is written as an extension: `FooBinding` above appears as `x-foo`. The key in the object model stays `foo`, so `channel.Bindings["foo"]` and `TryGetValue<FooBinding>` work as before. `AsyncApiBindingKeys.ToV3Key` returns the name a key is written under. AsyncAPI 2 allows any key, so AsyncAPI 2 documents keep `foo`.
54+
AsyncAPI 3 bindings objects only allow the binding names the specification lists (`amqp`, `http`, `kafka`, `ws` and so on) and `x-` extensions. When a document is serialized as AsyncAPI 3, a binding whose `BindingKey` is not one of those names is written as an extension: `FooBinding` above appears as `x-foo`. The key in the object model stays `foo`, so `channel.Bindings["foo"]` and `TryGetValue<FooBinding>` work as before. `AsyncApiBindingKeys.ToV3Key` returns the name a key is written under. AsyncAPI 2 allows any key, so AsyncAPI 2 documents keep `foo`. A binding whose `BindingKey` already starts with `x-` is written and read under that key in both versions.
5555

5656
When reading an AsyncAPI 3 document, `x-foo` is read as the binding if a parser for `foo` is registered; otherwise it stays an extension. A registered custom binding under its bare key (`foo`) is still read, but the reader reports an error, since the document does not conform to the specification.
5757

0 commit comments

Comments
 (0)