Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 54 additions & 10 deletions src/ByteBard.AsyncAPI.Readers/BindingDeserializer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace ByteBard.AsyncAPI.Readers
{
using System;
using System.Collections.Generic;
using System.Linq;
using ByteBard.AsyncAPI.Bindings;
using ByteBard.AsyncAPI.Exceptions;
using ByteBard.AsyncAPI.Extensions;
using ByteBard.AsyncAPI.Models;
Expand All @@ -25,29 +27,51 @@ public static T LoadBinding<T>(string nodeName, ParseNode node, FixedFieldMap<T>
internal static AsyncApiBindings<TBinding> LoadBindings<TBinding>(
MapNode mapNode,
IDictionary<string, IBindingParser<TBinding>> parsers,
PatternFieldMap<AsyncApiBindings<TBinding>> patternFields)
PatternFieldMap<AsyncApiBindings<TBinding>> patternFields,
AsyncApiVersion version = AsyncApiVersion.AsyncApi2_0)
where TBinding : IBinding
{
var context = mapNode.Context;
var bindings = new AsyncApiBindings<TBinding>();

// TBinding is the binding interface (e.g. IChannelBinding), so drop the leading "I" for the diagnostics.
var bindingType = typeof(TBinding).Name.Substring(1);
foreach (var property in mapNode)
{
if (patternFields.Keys.Any(isMatch => isMatch(property.Name)))
// A registered key is read as that binding, even when it carries the x- prefix itself.
var key = property.Name;
if (!parsers.ContainsKey(key))
{
continue;
if (version == AsyncApiVersion.AsyncApi3_0 && TryGetCustomBindingKey(key, parsers, out var customKey))
{
// AsyncAPI 3 writes a custom binding as an extension (x-<key>); read it back as the binding.
key = customKey;
}
else if (patternFields.Keys.Any(isMatch => isMatch(key)))
{
property.ParseField(bindings, patternFields);
continue;
}
}

if (!parsers.TryGetValue(property.Name, out var parser))
if (!parsers.TryGetValue(key, 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"));
new AsyncApiError(context.GetLocation(), $"{bindingType} {key} is not found"));
continue;
}

if (version == AsyncApiVersion.AsyncApi3_0 && AsyncApiBindingKeys.ToV3Key(key) != property.Name)
{
// AsyncAPI 3 bindings objects only allow the specification's binding names and extensions. The binding
// is still read, and written back as x-<key>.
context.Diagnostic.Errors.Add(
new AsyncApiError(
context.GetLocation(),
$"{bindingType} {key} is not an AsyncAPI 3 binding name; write a custom binding as {AsyncApiBindingKeys.ToV3Key(key)}"));
}

try
{
var binding = parser.LoadBinding(property);
Expand All @@ -63,11 +87,31 @@ internal static AsyncApiBindings<TBinding> LoadBindings<TBinding>(
}
}

mapNode.ParseFields(bindings, patternFields);

return bindings;
}

private static bool TryGetCustomBindingKey<TBinding>(
string propertyName,
IDictionary<string, IBindingParser<TBinding>> parsers,
out string key)
where TBinding : IBinding
{
key = null;
if (!propertyName.StartsWith(AsyncApiConstants.ExtensionFieldNamePrefix, StringComparison.Ordinal))
{
return false;
}

var candidate = propertyName.Substring(AsyncApiConstants.ExtensionFieldNamePrefix.Length);
if (AsyncApiBindingKeys.IsSpecificationKey(candidate) || !parsers.ContainsKey(candidate))
{
return false;
}

key = candidate;
return true;
}

private static PatternFieldMap<T> BindingPatternExtensionFields<T>()
where T : IBinding, new()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal static AsyncApiBindings<IChannelBinding> LoadChannelBindings(ParseNode
return new AsyncApiBindingsReference<IChannelBinding>(pointer);
}

return BindingDeserializer.LoadBindings(mapNode, node.Context.ChannelBindingParsers, channelBindingPatternFields);
return BindingDeserializer.LoadBindings(mapNode, node.Context.ChannelBindingParsers, channelBindingPatternFields, AsyncApiVersion.AsyncApi3_0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal static AsyncApiBindings<IMessageBinding> LoadMessageBindings(ParseNode
return new AsyncApiBindingsReference<IMessageBinding>(pointer);
}

return BindingDeserializer.LoadBindings(mapNode, node.Context.MessageBindingParsers, messageBindingPatternFields);
return BindingDeserializer.LoadBindings(mapNode, node.Context.MessageBindingParsers, messageBindingPatternFields, AsyncApiVersion.AsyncApi3_0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal static AsyncApiBindings<IOperationBinding> LoadOperationBindings(ParseN
return new AsyncApiBindingsReference<IOperationBinding>(pointer);
}

return BindingDeserializer.LoadBindings(mapNode, node.Context.OperationBindingParsers, operationBindingPatternFields);
return BindingDeserializer.LoadBindings(mapNode, node.Context.OperationBindingParsers, operationBindingPatternFields, AsyncApiVersion.AsyncApi3_0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal static AsyncApiBindings<IServerBinding> LoadServerBindings(ParseNode no
return new AsyncApiBindingsReference<IServerBinding>(pointer);
}

return BindingDeserializer.LoadBindings(mapNode, node.Context.ServerBindingParsers, serverBindingPatternFields);
return BindingDeserializer.LoadBindings(mapNode, node.Context.ServerBindingParsers, serverBindingPatternFields, AsyncApiVersion.AsyncApi3_0);
}
}
}
53 changes: 53 additions & 0 deletions src/ByteBard.AsyncAPI/Models/AsyncApiBindingKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace ByteBard.AsyncAPI.Bindings
{
using System;
using System.Collections.Generic;
using ByteBard.AsyncAPI.Models;
using ByteBard.AsyncAPI.Models.Interfaces;

/// <summary>
/// The binding names AsyncAPI 3 defines, and the key a binding is written under in an AsyncAPI 3 document.
/// </summary>
/// <remarks>
/// In AsyncAPI 3 the server, channel, operation and message bindings objects only allow the protocol names
/// the specification lists, plus <c>x-</c> extensions. A custom binding (one whose <see cref="IBinding.BindingKey"/>
/// is not listed) is therefore written as <c>x-&lt;key&gt;</c>. AsyncAPI 2 allows any key, so it keeps the key as is.
/// </remarks>
public static class AsyncApiBindingKeys
{
// Every binding the AsyncAPI 3.0 and 3.1 specifications name in a bindings object. The prose is normative; the
// JSON schemas omit mqtt5 and mercure, but the specification text lists them as fixed fields.
private static readonly HashSet<string> SpecificationKeys = new HashSet<string>(StringComparer.Ordinal)
{
"amqp", "amqp1", "anypointmq", "googlepubsub", "http", "ibmmq", "jms", "kafka", "mercure", "mqtt", "mqtt5",
"nats", "pulsar", "redis", "ros2", "sns", "solace", "sqs", "stomp", "ws",
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/// <summary>
/// Whether AsyncAPI 3 defines a binding with this name.
/// </summary>
/// <param name="key">The binding key.</param>
/// <returns><c>true</c> for a binding the specification lists; <c>false</c> otherwise, including for <c>x-</c> keys.</returns>
public static bool IsSpecificationKey(string key)
{
return key != null && SpecificationKeys.Contains(key);
}

/// <summary>
/// The property name a binding is written under in an AsyncAPI 3 bindings object.
/// </summary>
/// <param name="key">The binding key.</param>
/// <returns>The key itself for a specification binding or an <c>x-</c> key; otherwise the key with an <c>x-</c> prefix.</returns>
public static string ToV3Key(string key)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}

return IsSpecificationKey(key) || key.StartsWith(AsyncApiConstants.ExtensionFieldNamePrefix, StringComparison.Ordinal)
? key
: AsyncApiConstants.ExtensionFieldNamePrefix + key;
}
}
}
29 changes: 24 additions & 5 deletions src/ByteBard.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using ByteBard.AsyncAPI.Bindings;
using ByteBard.AsyncAPI.Models.Interfaces;
using ByteBard.AsyncAPI.Writers;

Expand Down Expand Up @@ -41,16 +42,34 @@ public virtual void SerializeV3(IAsyncApiWriter writer)
throw new ArgumentNullException(nameof(writer));
}

writer.WriteStartObject();
// AsyncAPI 3 only allows the specification's binding names, so a custom binding is written as an extension.
// Its x- name can then clash with an extension (or another binding) of the same name; refuse to write a
// document with a duplicate property rather than silently dropping one of them.
var names = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var extension in this.Extensions.Keys)
{
names[extension] = $"extension '{extension}'";
}

foreach (var binding in this)
{
var bindingType = binding.Key;
var bindingValue = binding.Value;
var name = AsyncApiBindingKeys.ToV3Key(binding.Key);
if (names.TryGetValue(name, out var existing))
{
throw new AsyncApiWriterException(
$"Binding '{binding.Key}' is written as '{name}' in AsyncAPI 3, which clashes with {existing}.");
}

names[name] = $"binding '{binding.Key}'";
}

writer.WritePropertyName(bindingType);
writer.WriteStartObject();

foreach (var binding in this)
{
writer.WritePropertyName(AsyncApiBindingKeys.ToV3Key(binding.Key));

bindingValue.SerializeV3(writer);
binding.Value.SerializeV3(writer);
}

writer.WriteExtensions(this.Extensions);
Expand Down
Loading
Loading