From 9939813b86681ce0641c1631048f5bca43ae6b78 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 12 Oct 2026 00:44:29 +0200 Subject: [PATCH 1/3] feat(bindings): add additional bindings --- .../AnypointMQ/AnypointMQChannelBinding.cs | 39 ++++ .../AnypointMQ/AnypointMQMessageBinding.cs | 45 ++++ .../BindingHelpers.cs | 7 +- .../BindingsCollection.cs | 109 ++++++---- .../GooglePubSubChannelBinding.cs | 55 +++++ .../GooglePubSubMessageBinding.cs | 44 ++++ .../GooglePubSub/MessageSchema.cs | 17 ++ .../GooglePubSub/MessageStoragePolicy.cs | 18 ++ .../GooglePubSub/SchemaSettings.cs | 23 ++ .../IBMMQ/IBMMQChannelBinding.cs | 56 +++++ .../IBMMQ/IBMMQMessageBinding.cs | 41 ++++ .../IBMMQ/IBMMQServerBinding.cs | 44 ++++ src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs | 21 ++ src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs | 23 ++ .../JMS/JMSChannelBinding.cs | 39 ++++ .../JMS/JMSMessageBinding.cs | 45 ++++ .../JMS/JMSProperty.cs | 22 ++ .../JMS/JMSServerBinding.cs | 50 +++++ .../Kafka/KafkaChannelBinding.cs | 24 ++- .../Kafka/TopicConfigurationObject.cs | 33 ++- .../MQTT/MQTTMessageBinding.cs | 26 ++- .../MQTT/MQTTOperationBinding.cs | 48 ++++- .../MQTT/MQTTServerBinding.cs | 47 ++++- .../MQTT/MQTTValueUnions.cs | 92 ++++++++ .../MQTT5/MQTT5ServerBinding.cs | 43 ++++ .../NATS/NATSOperationBinding.cs | 34 +++ .../ROS2/ROS2OperationBinding.cs | 37 ++++ .../ROS2/ROS2QosPolicies.cs | 43 ++++ .../ROS2/ROS2ServerBinding.cs | 38 ++++ .../Solace/SolaceDestination.cs | 38 ++++ .../Solace/SolaceIntegerOrSchema.cs | 62 ++++++ .../Solace/SolaceOperationBinding.cs | 63 ++++++ .../Solace/SolaceQueue.cs | 39 ++++ .../Solace/SolaceServerBinding.cs | 35 +++ .../Solace/SolaceTopic.cs | 27 +++ .../AnypointMQ/AnypointMQBindings_Should.cs | 45 ++++ .../GooglePubSubBindings_Should.cs | 54 +++++ .../Bindings/IBMMQ/IBMMQBindings_Should.cs | 70 ++++++ .../Bindings/JMS/JMSBindings_Should.cs | 60 ++++++ .../Bindings/Kafka/KafkaBindings_Should.cs | 34 +++ .../MQTT/MQTTBindingVariants_Should.cs | 115 ++++++++++ .../Bindings/MQTT5/MQTT5Bindings_Should.cs | 88 ++++++++ .../Bindings/NATS/NatsBindings_Should.cs | 31 +++ .../UnknownNestedField_Should.cs | 38 ++++ .../Bindings/ROS2/ROS2Bindings_Should.cs | 78 +++++++ .../Registration/BindingsCollection_Should.cs | 104 +++++++++ .../Bindings/Solace/SolaceBindings_Should.cs | 199 ++++++++++++++++++ 47 files changed, 2269 insertions(+), 74 deletions(-) create mode 100644 src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQMessageBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubChannelBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubMessageBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageSchema.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageStoragePolicy.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/GooglePubSub/SchemaSettings.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQChannelBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/JMS/JMSChannelBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/JMS/JMSMessageBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/JMS/JMSProperty.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/JMS/JMSServerBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/NATS/NATSOperationBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2OperationBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/Solace/SolaceDestination.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/Solace/SolaceQueue.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/Solace/SolaceServerBinding.cs create mode 100644 src/ByteBard.AsyncAPI.Bindings/Solace/SolaceTopic.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/AnypointMQ/AnypointMQBindings_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/GooglePubSub/GooglePubSubBindings_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/IBMMQ/IBMMQBindings_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/JMS/JMSBindings_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/MQTT5/MQTT5Bindings_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/NATS/NatsBindings_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/NestedParsing/UnknownNestedField_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/ROS2/ROS2Bindings_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/Registration/BindingsCollection_Should.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/Bindings/Solace/SolaceBindings_Should.cs diff --git a/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs new file mode 100644 index 0000000..eb22c6f --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs @@ -0,0 +1,39 @@ +namespace ByteBard.AsyncAPI.Bindings.AnypointMQ +{ + using System; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding for an Anypoint MQ destination. + public class AnypointMQChannelBinding : ChannelBinding + { + public string Destination { get; set; } + + public string DestinationType { get; set; } + + public override string BindingKey => "anypointmq"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "destination", (a, n) => { a.Destination = n.GetScalarValue(); } }, + { "destinationType", (a, n) => { a.DestinationType = n.GetScalarValue(); } }, + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("destination", this.Destination); + writer.WriteOptionalProperty("destinationType", this.DestinationType); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQMessageBinding.cs new file mode 100644 index 0000000..1c3395d --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQMessageBinding.cs @@ -0,0 +1,45 @@ +namespace ByteBard.AsyncAPI.Bindings.AnypointMQ +{ + using System; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding for an Anypoint MQ message. + public class AnypointMQMessageBinding : MessageBinding + { + /// Message headers described by a schema or schema reference. + public AsyncApiJsonSchema Headers { get; set; } + + public override string BindingKey => "anypointmq"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalObject("headers", this.Headers, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalObject("headers", this.Headers, (w, h) => h.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs b/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs index 219e79e..3d83116 100644 --- a/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs +++ b/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs @@ -1,5 +1,7 @@ namespace ByteBard.AsyncAPI.Bindings { + using System; + using System.Collections.Generic; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Readers; using ByteBard.AsyncAPI.Readers.ParseNodes; @@ -16,10 +18,11 @@ public static T ParseMap(this ParseNode node, FixedFieldMap fixedFieldMap) } var instance = new T(); + var emptyPatternFields = new Dictionary, Action>(); foreach (var propertyNode in mapNode) { - propertyNode.ParseField(instance, fixedFieldMap, null); + propertyNode.ParseField(instance, fixedFieldMap, emptyPatternFields); } return instance; @@ -44,4 +47,4 @@ public static T ParseMapWithExtensions(this ParseNode node, FixedFieldMap return instance; } } -} \ No newline at end of file +} diff --git a/src/ByteBard.AsyncAPI.Bindings/BindingsCollection.cs b/src/ByteBard.AsyncAPI.Bindings/BindingsCollection.cs index 5801c24..2d291a4 100644 --- a/src/ByteBard.AsyncAPI.Bindings/BindingsCollection.cs +++ b/src/ByteBard.AsyncAPI.Bindings/BindingsCollection.cs @@ -1,13 +1,21 @@ -namespace ByteBard.AsyncAPI.Bindings +namespace ByteBard.AsyncAPI.Bindings { using System; using System.Collections.Generic; using ByteBard.AsyncAPI.Bindings.AMQP; + using ByteBard.AsyncAPI.Bindings.AnypointMQ; + using ByteBard.AsyncAPI.Bindings.GooglePubSub; using ByteBard.AsyncAPI.Bindings.Http; + using ByteBard.AsyncAPI.Bindings.IBMMQ; + using ByteBard.AsyncAPI.Bindings.JMS; using ByteBard.AsyncAPI.Bindings.Kafka; using ByteBard.AsyncAPI.Bindings.MQTT; + using ByteBard.AsyncAPI.Bindings.MQTT5; + using ByteBard.AsyncAPI.Bindings.NATS; using ByteBard.AsyncAPI.Bindings.Pulsar; + using ByteBard.AsyncAPI.Bindings.ROS2; using ByteBard.AsyncAPI.Bindings.Sns; + using ByteBard.AsyncAPI.Bindings.Solace; using ByteBard.AsyncAPI.Bindings.Sqs; using ByteBard.AsyncAPI.Bindings.WebSockets; using ByteBard.AsyncAPI.Models.Interfaces; @@ -15,51 +23,30 @@ public static class BindingsCollection { - public static TCollection Add( - this TCollection destination, - IEnumerable source) - where TCollection : ICollection + public static TCollection Add(this TCollection destination, IEnumerable source) + where TCollection : ICollection { - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } - - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - + if (destination == null) throw new ArgumentNullException(nameof(destination)); + if (source == null) throw new ArgumentNullException(nameof(source)); if (destination is List list) { list.AddRange(source); return destination; } - foreach (var item in source) - { - destination.Add(item); - } - + foreach (var item in source) destination.Add(item); return destination; } public static ICollection> All => new List> { - Pulsar, - Kafka, - Http, - Websockets, - Sqs, - Sns, - AMQP, - MQTT, + Pulsar, Kafka, Http, Websockets, Sqs, Sns, AMQP, MQTT, AnypointMQ, GooglePubSub, + IBMMQ, JMS, MQTT5, NATS, ROS2, Solace, }; public static ICollection> Http => new List> { - new HttpOperationBinding(), - new HttpMessageBinding(), + new HttpOperationBinding(), new HttpMessageBinding(), }; public static ICollection> Websockets => new List> @@ -69,42 +56,72 @@ public static TCollection Add( public static ICollection> Kafka => new List> { - new KafkaServerBinding(), - new KafkaChannelBinding(), - new KafkaOperationBinding(), - new KafkaMessageBinding(), + new KafkaServerBinding(), new KafkaChannelBinding(), new KafkaOperationBinding(), new KafkaMessageBinding(), }; public static ICollection> Pulsar => new List> { - new PulsarServerBinding(), - new PulsarChannelBinding(), + new PulsarServerBinding(), new PulsarChannelBinding(), }; public static ICollection> Sqs => new List> { - new SqsChannelBinding(), - new SqsOperationBinding(), + new SqsChannelBinding(), new SqsOperationBinding(), }; public static ICollection> Sns => new List> { - new SnsChannelBinding(), - new SnsOperationBinding(), + new SnsChannelBinding(), new SnsOperationBinding(), }; public static ICollection> AMQP => new List> { - new AMQPChannelBinding(), - new AMQPOperationBinding(), - new AMQPMessageBinding(), + new AMQPChannelBinding(), new AMQPOperationBinding(), new AMQPMessageBinding(), + }; + + public static ICollection> AnypointMQ => new List> + { + new AnypointMQChannelBinding(), new AnypointMQMessageBinding(), + }; + + public static ICollection> GooglePubSub => new List> + { + new GooglePubSubChannelBinding(), new GooglePubSubMessageBinding(), + }; + + public static ICollection> IBMMQ => new List> + { + new IBMMQServerBinding(), new IBMMQChannelBinding(), new IBMMQMessageBinding(), + }; + + public static ICollection> JMS => new List> + { + new JMSServerBinding(), new JMSChannelBinding(), new JMSMessageBinding(), }; public static ICollection> MQTT => new List> { - new MQTTServerBinding(), - new MQTTOperationBinding(), - new MQTTMessageBinding(), + new MQTTServerBinding(), new MQTTOperationBinding(), new MQTTMessageBinding(), + }; + + public static ICollection> MQTT5 => new List> + { + new MQTT5ServerBinding(), + }; + + public static ICollection> NATS => new List> + { + new NATSOperationBinding(), + }; + + public static ICollection> ROS2 => new List> + { + new ROS2ServerBinding(), new ROS2OperationBinding(), + }; + + public static ICollection> Solace => new List> + { + new SolaceServerBinding(), new SolaceOperationBinding(), }; } } diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubChannelBinding.cs new file mode 100644 index 0000000..3f1f3e1 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubChannelBinding.cs @@ -0,0 +1,55 @@ +namespace ByteBard.AsyncAPI.Bindings.GooglePubSub +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + public class GooglePubSubChannelBinding : ChannelBinding + { + public Dictionary Labels { get; set; } + public string MessageRetentionDuration { get; set; } + public MessageStoragePolicy MessageStoragePolicy { get; set; } + public SchemaSettings SchemaSettings { get; set; } + + public override string BindingKey => "googlepubsub"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "labels", (a, n) => { a.Labels = n.CreateSimpleMap(s => s.GetScalarValue()); } }, + { "messageRetentionDuration", (a, n) => { a.MessageRetentionDuration = n.GetScalarValue(); } }, + { "messageStoragePolicy", (a, n) => { a.MessageStoragePolicy = n.ParseMap(messageStoragePolicyFields); } }, + { "schemaSettings", (a, n) => { a.SchemaSettings = n.ParseMap(schemaSettingsFields); } }, + }; + + private static readonly FixedFieldMap messageStoragePolicyFields = new() + { + { "allowedPersistenceRegions", (a, n) => { a.AllowedPersistenceRegions = n.CreateSimpleList(s => s.GetScalarValue()); } }, + }; + + private static readonly FixedFieldMap schemaSettingsFields = new() + { + { "encoding", (a, n) => { a.Encoding = n.GetScalarValue(); } }, + { "firstRevisionId", (a, n) => { a.FirstRevisionId = n.GetScalarValue(); } }, + { "lastRevisionId", (a, n) => { a.LastRevisionId = n.GetScalarValue(); } }, + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalMap("labels", this.Labels, (w, v) => w.WriteValue(v)); + writer.WriteOptionalProperty("messageRetentionDuration", this.MessageRetentionDuration); + writer.WriteOptionalObject("messageStoragePolicy", this.MessageStoragePolicy, (w, v) => v.Serialize(w)); + writer.WriteOptionalObject("schemaSettings", this.SchemaSettings, (w, v) => v.Serialize(w)); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubMessageBinding.cs new file mode 100644 index 0000000..20774af --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubMessageBinding.cs @@ -0,0 +1,44 @@ +namespace ByteBard.AsyncAPI.Bindings.GooglePubSub +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + public class GooglePubSubMessageBinding : MessageBinding + { + public Dictionary Attributes { get; set; } + public string OrderingKey { get; set; } + public MessageSchema Schema { get; set; } + + public override string BindingKey => "googlepubsub"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "attributes", (a, n) => { a.Attributes = n.CreateSimpleMap(s => s.GetScalarValue()); } }, + { "orderingKey", (a, n) => { a.OrderingKey = n.GetScalarValue(); } }, + { "schema", (a, n) => { a.Schema = n.ParseMap(schemaFields); } }, + }; + + private static readonly FixedFieldMap schemaFields = new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalMap("attributes", this.Attributes, (w, v) => w.WriteValue(v)); + writer.WriteOptionalProperty("orderingKey", this.OrderingKey); + writer.WriteOptionalObject("schema", this.Schema, (w, v) => v.Serialize(w)); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageSchema.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageSchema.cs new file mode 100644 index 0000000..e31c71e --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageSchema.cs @@ -0,0 +1,17 @@ +namespace ByteBard.AsyncAPI.Bindings.GooglePubSub +{ + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + public class MessageSchema : IAsyncApiElement + { + public string Name { get; set; } + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageStoragePolicy.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageStoragePolicy.cs new file mode 100644 index 0000000..73f8462 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageStoragePolicy.cs @@ -0,0 +1,18 @@ +namespace ByteBard.AsyncAPI.Bindings.GooglePubSub +{ + using System.Collections.Generic; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + public class MessageStoragePolicy : IAsyncApiElement + { + public IEnumerable AllowedPersistenceRegions { get; set; } + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalCollection("allowedPersistenceRegions", this.AllowedPersistenceRegions, (w, v) => w.WriteValue(v)); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/SchemaSettings.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/SchemaSettings.cs new file mode 100644 index 0000000..8516cac --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/SchemaSettings.cs @@ -0,0 +1,23 @@ +namespace ByteBard.AsyncAPI.Bindings.GooglePubSub +{ + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + public class SchemaSettings : IAsyncApiElement + { + public string Encoding { get; set; } + public string FirstRevisionId { get; set; } + public string LastRevisionId { get; set; } + public string Name { get; set; } + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("encoding", this.Encoding); + writer.WriteOptionalProperty("firstRevisionId", this.FirstRevisionId); + writer.WriteOptionalProperty("lastRevisionId", this.LastRevisionId); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQChannelBinding.cs new file mode 100644 index 0000000..9bdc2b5 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQChannelBinding.cs @@ -0,0 +1,56 @@ +namespace ByteBard.AsyncAPI.Bindings.IBMMQ +{ + using System; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + public class IBMMQChannelBinding : ChannelBinding + { + public string DestinationType { get; set; } + public Queue Queue { get; set; } + public Topic Topic { get; set; } + public int? MaxMsgLength { get; set; } + + public override string BindingKey => "ibmmq"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "destinationType", (a, n) => { a.DestinationType = n.GetScalarValue(); } }, + { "queue", (a, n) => { a.Queue = n.ParseMap(queueFields); } }, + { "topic", (a, n) => { a.Topic = n.ParseMap(topicFields); } }, + { "maxMsgLength", (a, n) => { a.MaxMsgLength = n.GetIntegerValue(); } }, + }; + + private static readonly FixedFieldMap queueFields = new() + { + { "objectName", (a, n) => { a.ObjectName = n.GetScalarValue(); } }, + { "isPartitioned", (a, n) => { a.IsPartitioned = n.GetBooleanValue(); } }, + { "exclusive", (a, n) => { a.Exclusive = n.GetBooleanValue(); } }, + }; + + private static readonly FixedFieldMap topicFields = new() + { + { "string", (a, n) => { a.StringValue = n.GetScalarValue(); } }, + { "objectName", (a, n) => { a.ObjectName = n.GetScalarValue(); } }, + { "durablePermitted", (a, n) => { a.DurablePermitted = n.GetBooleanValue(); } }, + { "lastMsgRetained", (a, n) => { a.LastMsgRetained = n.GetBooleanValue(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("destinationType", this.DestinationType); + writer.WriteOptionalObject("queue", this.Queue, (w, v) => v.Serialize(w)); + writer.WriteOptionalObject("topic", this.Topic, (w, v) => v.Serialize(w)); + writer.WriteOptionalProperty("maxMsgLength", this.MaxMsgLength); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs new file mode 100644 index 0000000..bbffc24 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs @@ -0,0 +1,41 @@ +namespace ByteBard.AsyncAPI.Bindings.IBMMQ +{ + using System; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + public class IBMMQMessageBinding : MessageBinding + { + public string Type { get; set; } + public string Headers { get; set; } + public string Description { get; set; } + public int? Expiry { get; set; } + + public override string BindingKey => "ibmmq"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "type", (a, n) => { a.Type = n.GetScalarValue(); } }, + { "headers", (a, n) => { a.Headers = n.GetScalarValue(); } }, + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "expiry", (a, n) => { a.Expiry = n.GetIntegerValue(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteOptionalProperty("headers", this.Headers); + writer.WriteOptionalProperty("description", this.Description); + writer.WriteOptionalProperty("expiry", this.Expiry); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs new file mode 100644 index 0000000..cff2537 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs @@ -0,0 +1,44 @@ +namespace ByteBard.AsyncAPI.Bindings.IBMMQ +{ + using System; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + public class IBMMQServerBinding : ServerBinding + { + public string GroupId { get; set; } + public string CcdtQueueManagerName { get; set; } + public string CipherSpec { get; set; } + public bool? MultiEndpointServer { get; set; } + public int? HeartBeatInterval { get; set; } + + public override string BindingKey => "ibmmq"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "groupId", (a, n) => { a.GroupId = n.GetScalarValue(); } }, + { "ccdtQueueManagerName", (a, n) => { a.CcdtQueueManagerName = n.GetScalarValue(); } }, + { "cipherSpec", (a, n) => { a.CipherSpec = n.GetScalarValue(); } }, + { "multiEndpointServer", (a, n) => { a.MultiEndpointServer = n.GetBooleanValue(); } }, + { "heartBeatInterval", (a, n) => { a.HeartBeatInterval = n.GetIntegerValue(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("groupId", this.GroupId); + writer.WriteOptionalProperty("ccdtQueueManagerName", this.CcdtQueueManagerName); + writer.WriteOptionalProperty("cipherSpec", this.CipherSpec); + writer.WriteOptionalProperty("multiEndpointServer", this.MultiEndpointServer); + writer.WriteOptionalProperty("heartBeatInterval", this.HeartBeatInterval); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs new file mode 100644 index 0000000..9b2f5d4 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs @@ -0,0 +1,21 @@ +namespace ByteBard.AsyncAPI.Bindings.IBMMQ +{ + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + public class Queue : IAsyncApiElement + { + public string ObjectName { get; set; } + public bool? IsPartitioned { get; set; } + public bool? Exclusive { get; set; } + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("objectName", this.ObjectName); + writer.WriteOptionalProperty("isPartitioned", this.IsPartitioned); + writer.WriteOptionalProperty("exclusive", this.Exclusive); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs new file mode 100644 index 0000000..2c6bb46 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs @@ -0,0 +1,23 @@ +namespace ByteBard.AsyncAPI.Bindings.IBMMQ +{ + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + public class Topic : IAsyncApiElement + { + public string StringValue { get; set; } + public string ObjectName { get; set; } + public bool? DurablePermitted { get; set; } + public bool? LastMsgRetained { get; set; } + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("string", this.StringValue); + writer.WriteOptionalProperty("objectName", this.ObjectName); + writer.WriteOptionalProperty("durablePermitted", this.DurablePermitted); + writer.WriteOptionalProperty("lastMsgRetained", this.LastMsgRetained); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSChannelBinding.cs new file mode 100644 index 0000000..d80967a --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSChannelBinding.cs @@ -0,0 +1,39 @@ +namespace ByteBard.AsyncAPI.Bindings.JMS +{ + using System; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding for a JMS destination channel. + public class JMSChannelBinding : ChannelBinding + { + public string Destination { get; set; } + + public string DestinationType { get; set; } + + public override string BindingKey => "jms"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "destination", (a, n) => { a.Destination = n.GetScalarValue(); } }, + { "destinationType", (a, n) => { a.DestinationType = n.GetScalarValue(); } }, + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("destination", this.Destination); + writer.WriteOptionalProperty("destinationType", this.DestinationType); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSMessageBinding.cs new file mode 100644 index 0000000..a95b558 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSMessageBinding.cs @@ -0,0 +1,45 @@ +namespace ByteBard.AsyncAPI.Bindings.JMS +{ + using System; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding for a JMS message. + public class JMSMessageBinding : MessageBinding + { + /// Schema describing the JMS message headers. + public AsyncApiJsonSchema Headers { get; set; } + + public override string BindingKey => "jms"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalObject("headers", this.Headers, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalObject("headers", this.Headers, (w, h) => h.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSProperty.cs b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSProperty.cs new file mode 100644 index 0000000..b9872d8 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSProperty.cs @@ -0,0 +1,22 @@ +namespace ByteBard.AsyncAPI.Bindings.JMS +{ + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + /// A name and typed value for a JMS connection property. + public class JMSProperty : IAsyncApiElement + { + public string Name { get; set; } + + public AsyncApiAny Value { get; set; } + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteOptionalObject("value", this.Value, (w, v) => w.WriteAny(v)); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSServerBinding.cs new file mode 100644 index 0000000..198cc13 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSServerBinding.cs @@ -0,0 +1,50 @@ +namespace ByteBard.AsyncAPI.Bindings.JMS +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding for a JMS server connection. + public class JMSServerBinding : ServerBinding + { + public string JmsConnectionFactory { get; set; } + + public IList Properties { get; set; } + + public string ClientID { get; set; } + + protected override FixedFieldMap FixedFieldMap => new() + { + { "jmsConnectionFactory", (a, n) => { a.JmsConnectionFactory = n.GetScalarValue(); } }, + { "properties", (a, n) => { a.Properties = n.CreateList(node => node.ParseMap(JMSPropertyFixedFields)); } }, + { "clientID", (a, n) => { a.ClientID = n.GetScalarValue(); } }, + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + }; + + private static readonly FixedFieldMap JMSPropertyFixedFields = new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "value", (a, n) => { a.Value = n.CreateAny(); } }, + }; + + public override string BindingKey => "jms"; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteRequiredProperty("jmsConnectionFactory", this.JmsConnectionFactory); + writer.WriteOptionalCollection("properties", this.Properties, (w, p) => p.Serialize(w)); + writer.WriteOptionalProperty("clientID", this.ClientID); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs index bb96af5..aa999ba 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs @@ -1,6 +1,7 @@ namespace ByteBard.AsyncAPI.Bindings.Kafka { using System; + using System.Collections.Generic; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; using ByteBard.AsyncAPI.Writers; @@ -37,7 +38,7 @@ public class KafkaChannelBinding : ChannelBinding { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "topic", (a, n) => { a.Topic = n.GetScalarValue(); } }, { "partitions", (a, n) => { a.Partitions = n.GetIntegerValue(); } }, - { "topicConfiguration", (a, n) => { a.TopicConfiguration = n.ParseMap(kafkaChannelTopicConfigurationObjectFixedFields); } }, + { "topicConfiguration", (a, n) => { a.TopicConfiguration = ParseTopicConfiguration(n); } }, { "replicas", (a, n) => { a.Replicas = n.GetIntegerValue(); } }, }; @@ -54,6 +55,27 @@ public class KafkaChannelBinding : ChannelBinding { "confluent.value.subject.name.strategy", (a, n) => { a.ConfluentValueSubjectName = n.GetScalarValue(); } }, }; + private static TopicConfigurationObject ParseTopicConfiguration(ParseNode node) + { + var mapNode = node.CheckMapNode(node.Context.GetLocation()); + var configuration = new TopicConfigurationObject(); + var emptyPatternFields = new Dictionary, Action>(); + + foreach (var propertyNode in mapNode) + { + if (kafkaChannelTopicConfigurationObjectFixedFields.ContainsKey(propertyNode.Name)) + { + propertyNode.ParseField(configuration, kafkaChannelTopicConfigurationObjectFixedFields, emptyPatternFields); + } + else + { + configuration.AdditionalProperties[propertyNode.Name] = propertyNode.Value.CreateAny(); + } + } + + return configuration; + } + public override void SerializeV2(IAsyncApiWriter writer) { this.SerializeV3(writer); diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs index 860db80..3a9d410 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs @@ -8,6 +8,19 @@ public class TopicConfigurationObject : IAsyncApiElement { + private static readonly HashSet FixedFieldNames = new() + { + AsyncApiConstants.CleanupPolicy, + AsyncApiConstants.RetentionMilliseconds, + AsyncApiConstants.RetentionBytes, + AsyncApiConstants.DeleteRetentionMilliseconds, + AsyncApiConstants.MaxMessageBytes, + AsyncApiConstants.ConfluentKeySchemaValidation, + AsyncApiConstants.ConfluentKeySubjectName, + AsyncApiConstants.ConfluentValueSchemaValidation, + AsyncApiConstants.ConfluentValueSubjectName, + }; + /// /// The cleanup.policy configuration option. /// @@ -53,6 +66,12 @@ public class TopicConfigurationObject : IAsyncApiElement /// public string ConfluentValueSubjectName { get; set; } + /// + /// Additional Kafka topic configuration values not represented by fixed properties. + /// Values are retained as JSON-backed instances. + /// + public Dictionary AdditionalProperties { get; set; } = new(); + public void Serialize(IAsyncApiWriter writer) { if (writer is null) @@ -70,7 +89,19 @@ public void Serialize(IAsyncApiWriter writer) writer.WriteOptionalProperty(AsyncApiConstants.ConfluentKeySubjectName, this.ConfluentKeySubjectName); writer.WriteOptionalProperty(AsyncApiConstants.ConfluentValueSchemaValidation, this.ConfluentValueSchemaValidation); writer.WriteOptionalProperty(AsyncApiConstants.ConfluentValueSubjectName, this.ConfluentValueSubjectName); + if (this.AdditionalProperties != null) + { + foreach (var property in this.AdditionalProperties) + { + if (!FixedFieldNames.Contains(property.Key)) + { + writer.WritePropertyName(property.Key); + writer.WriteAny(property.Value); + } + } + } + writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs index 44cec88..bcb72a3 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -31,19 +31,31 @@ public class MQTTMessageBinding : MessageBinding /// /// The topic (channel URI) for a response message. /// - public string ResponseTopic { get; set; } + public string ResponseTopic + { + get => this.ResponseTopicValue?.Value; + set => this.ResponseTopicValue = value == null ? null : MQTTStringOrSchema.FromString(value); + } + + /// The string-or-schema response topic value. + public MQTTStringOrSchema ResponseTopicValue { get; set; } public override void SerializeV2(IAsyncApiWriter writer) { - this.SerializeV3(writer); + this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); } public override void SerializeV3(IAsyncApiWriter writer) { - this.SerializeProperties(writer); + this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); } public override void SerializeProperties(IAsyncApiWriter writer) + { + this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); + } + + private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version) { if (writer is null) { @@ -52,9 +64,10 @@ public override void SerializeProperties(IAsyncApiWriter writer) writer.WriteStartObject(); writer.WriteOptionalProperty("payloadFormatIndicator", this.PayloadFormatIndicator); - writer.WriteOptionalObject("correlationData", this.CorrelationData, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalObject("correlationData", this.CorrelationData, (w, h) => { if (version == AsyncApiVersion.AsyncApi2_0) h.SerializeV2(w); else h.SerializeV3(w); }); writer.WriteOptionalProperty("contentType", this.ContentType); - writer.WriteOptionalProperty("responseTopic", this.ResponseTopic); + if (this.ResponseTopicValue?.Value != null) writer.WriteOptionalProperty("responseTopic", this.ResponseTopicValue.Value); + else if (this.ResponseTopicValue?.Schema != null) writer.WriteOptionalObject("responseTopic", this.ResponseTopicValue.Schema, (w, schema) => { if (version == AsyncApiVersion.AsyncApi2_0) schema.SerializeV2(w); else schema.SerializeV3(w); }); writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); @@ -64,10 +77,11 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "payloadFormatIndicator", (a, n) => { a.PayloadFormatIndicator = n.GetIntegerValueOrDefault(); } }, { "correlationData", (a, n) => { a.CorrelationData = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, - { "responseTopic", (a, n) => { a.ResponseTopic = n.GetScalarValue(); } }, + { "responseTopic", (a, n) => { a.ResponseTopicValue = MQTTStringOrSchema.Parse(n); } }, }; } } diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs index fb593aa..7659b21 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs @@ -1,6 +1,7 @@ namespace ByteBard.AsyncAPI.Bindings.MQTT { using System; + using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; using ByteBard.AsyncAPI.Writers; @@ -9,42 +10,66 @@ /// public class MQTTOperationBinding : OperationBinding { + private int? qos; + private bool? retain; + /// /// Defines the Quality of Service (QoS) levels for the message flow between client and server. /// Its value MUST be either 0 (At most once delivery), 1 (At least once delivery), or 2 (Exactly once delivery). /// - public int QoS { get; set; } + public int QoS + { + get => this.qos ?? 0; + set => this.qos = value; + } /// /// Whether the broker should retain the message or not. /// - public bool Retain { get; set; } + public bool Retain + { + get => this.retain ?? false; + set => this.retain = value; + } /// /// Interval in seconds or a Schema Object containing the definition of the lifetime of the message. /// - public int? MessageExpiryInterval { get; set; } + public int? MessageExpiryInterval + { + get => this.MessageExpiryIntervalValue?.Integer; + set => this.MessageExpiryIntervalValue = value.HasValue ? MQTTIntegerOrSchema.FromInteger(value.Value) : null; + } + + /// The integer-or-schema message expiry interval value. + public MQTTIntegerOrSchema MessageExpiryIntervalValue { get; set; } public override string BindingKey => "mqtt"; protected override FixedFieldMap FixedFieldMap => new() { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "qos", (a, n) => { a.QoS = n.GetIntegerValue(); } }, { "retain", (a, n) => { a.Retain = n.GetBooleanValue(); } }, - { "messageExpiryInterval", (a, n) => { a.MessageExpiryInterval = n.GetIntegerValueOrDefault(); } }, + { "messageExpiryInterval", (a, n) => { a.MessageExpiryIntervalValue = MQTTIntegerOrSchema.Parse(n); } }, }; public override void SerializeV2(IAsyncApiWriter writer) { - this.SerializeV3(writer); + this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); } public override void SerializeV3(IAsyncApiWriter writer) { - this.SerializeProperties(writer); + this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); } public override void SerializeProperties(IAsyncApiWriter writer) + { + this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); + } + + private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version) { if (writer is null) { @@ -52,9 +77,14 @@ public override void SerializeProperties(IAsyncApiWriter writer) } writer.WriteStartObject(); - writer.WriteRequiredProperty("qos", this.QoS); - writer.WriteRequiredProperty("retain", this.Retain); - writer.WriteOptionalProperty("messageExpiryInterval", this.MessageExpiryInterval); + writer.WriteOptionalProperty("qos", this.qos); + writer.WriteOptionalProperty("retain", this.retain); + if (this.MessageExpiryIntervalValue?.LongInteger is long integer) writer.WriteRequiredProperty("messageExpiryInterval", integer); + else if (this.MessageExpiryIntervalValue?.Schema != null) writer.WriteOptionalObject("messageExpiryInterval", this.MessageExpiryIntervalValue.Schema, (w, schema) => + { + if (version == AsyncApiVersion.AsyncApi2_0) schema.SerializeV2(w); + else schema.SerializeV3(w); + }); writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs index a137f8b..b7b2302 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs @@ -37,13 +37,27 @@ public class MQTTServerBinding : ServerBinding /// Interval in seconds the broker maintains a session /// for a disconnected client until this interval expires. /// - public int? SessionExpiryInterval { get; set; } + public int? SessionExpiryInterval + { + get => this.SessionExpiryIntervalValue?.Integer; + set => this.SessionExpiryIntervalValue = value.HasValue ? MQTTIntegerOrSchema.FromInteger(value.Value) : null; + } + + /// The integer-or-schema session expiry interval value. + public MQTTIntegerOrSchema SessionExpiryIntervalValue { get; set; } /// /// Number of bytes representing the maximum packet size /// the client is willing to accept. /// - public int? MaximumPacketSize { get; set; } + public int? MaximumPacketSize + { + get => this.MaximumPacketSizeValue?.Integer; + set => this.MaximumPacketSizeValue = value.HasValue ? MQTTIntegerOrSchema.FromInteger(value.Value) : null; + } + + /// The integer-or-schema maximum packet size value. + public MQTTIntegerOrSchema MaximumPacketSizeValue { get; set; } public override string BindingKey => "mqtt"; @@ -54,8 +68,8 @@ public class MQTTServerBinding : ServerBinding { "cleanSession", (a, n) => { a.CleanSession = n.GetBooleanValueOrDefault(); } }, { "lastWill", (a, n) => { a.LastWill = n.ParseMap(LastWillFixedFields); } }, { "keepAlive", (a, n) => { a.KeepAlive = n.GetIntegerValueOrDefault(); } }, - { "sessionExpiryInterval", (a, n) => { a.SessionExpiryInterval = n.GetIntegerValueOrDefault(); } }, - { "maximumPacketSize", (a, n) => { a.MaximumPacketSize = n.GetIntegerValueOrDefault(); } }, + { "sessionExpiryInterval", (a, n) => { a.SessionExpiryIntervalValue = MQTTIntegerOrSchema.Parse(n); } }, + { "maximumPacketSize", (a, n) => { a.MaximumPacketSizeValue = MQTTIntegerOrSchema.Parse(n); } }, }; private static FixedFieldMap LastWillFixedFields = new() @@ -68,15 +82,20 @@ public class MQTTServerBinding : ServerBinding public override void SerializeV2(IAsyncApiWriter writer) { - this.SerializeV3(writer); + this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); } public override void SerializeV3(IAsyncApiWriter writer) { - this.SerializeProperties(writer); + this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); } public override void SerializeProperties(IAsyncApiWriter writer) + { + this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); + } + + private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version) { if (writer is null) { @@ -84,15 +103,25 @@ public override void SerializeProperties(IAsyncApiWriter writer) } writer.WriteStartObject(); - writer.WriteRequiredProperty("clientId", this.ClientId); + writer.WriteOptionalProperty("clientId", this.ClientId); writer.WriteOptionalProperty("cleanSession", this.CleanSession); writer.WriteOptionalObject("lastWill", this.LastWill, (w, l) => l.Serialize(w)); writer.WriteOptionalProperty("keepAlive", this.KeepAlive); - writer.WriteOptionalProperty("sessionExpiryInterval", this.SessionExpiryInterval); - writer.WriteOptionalProperty("maximumPacketSize", this.MaximumPacketSize); + this.WriteIntegerOrSchema(writer, "sessionExpiryInterval", this.SessionExpiryIntervalValue, version); + this.WriteIntegerOrSchema(writer, "maximumPacketSize", this.MaximumPacketSizeValue, version); writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } + + private void WriteIntegerOrSchema(IAsyncApiWriter writer, string name, MQTTIntegerOrSchema value, AsyncApiVersion version) + { + if (value?.LongInteger is long integer) writer.WriteRequiredProperty(name, integer); + else if (value?.Schema != null) writer.WriteOptionalObject(name, value.Schema, (w, schema) => + { + if (version == AsyncApiVersion.AsyncApi2_0) schema.SerializeV2(w); + else schema.SerializeV3(w); + }); + } } } diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs new file mode 100644 index 0000000..b10fa18 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs @@ -0,0 +1,92 @@ +namespace ByteBard.AsyncAPI.Bindings.MQTT +{ + using System; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// A MQTT binding value represented by either an integer or a JSON Schema. + public sealed class MQTTIntegerOrSchema + { + private MQTTIntegerOrSchema(long? integer, AsyncApiJsonSchema schema) + { + this.LongInteger = integer; + this.Schema = schema; + } + + /// The integer value when it is within the range of the legacy int API. + public int? Integer => this.LongInteger.HasValue && this.LongInteger.Value >= int.MinValue && this.LongInteger.Value <= int.MaxValue + ? (int)this.LongInteger.Value + : null; + + /// The full-width integer value. + public long? LongInteger { get; } + + public AsyncApiJsonSchema Schema { get; } + + public static MQTTIntegerOrSchema FromInteger(int value) => new(value, null); + + public static MQTTIntegerOrSchema FromLong(long value) => new(value, null); + + public static MQTTIntegerOrSchema FromSchema(AsyncApiJsonSchema schema) => new(null, schema ?? throw new ArgumentNullException(nameof(schema))); + + public static MQTTIntegerOrSchema Parse(ParseNode node) + { + return node is ValueNode + ? FromLong(node.GetLongValue()) + : FromSchema(AsyncApiJsonSchemaDeserializer.LoadSchema(node)); + } + + public void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) + { + if (this.LongInteger.HasValue) + { + writer.WriteValue(this.LongInteger.Value); + } + else if (this.Schema != null) + { + if (version == AsyncApiVersion.AsyncApi2_0) this.Schema.SerializeV2(writer); + else this.Schema.SerializeV3(writer); + } + } + } + + /// A MQTT message response topic represented by either a string or a JSON Schema. + public sealed class MQTTStringOrSchema + { + private MQTTStringOrSchema(string value, AsyncApiJsonSchema schema) + { + this.Value = value; + this.Schema = schema; + } + + public string Value { get; } + + public AsyncApiJsonSchema Schema { get; } + + public static MQTTStringOrSchema FromString(string value) => new(value ?? throw new ArgumentNullException(nameof(value)), null); + + public static MQTTStringOrSchema FromSchema(AsyncApiJsonSchema schema) => new(null, schema ?? throw new ArgumentNullException(nameof(schema))); + + public static MQTTStringOrSchema Parse(ParseNode node) + { + return node is ValueNode + ? FromString(node.GetScalarValue()) + : FromSchema(AsyncApiJsonSchemaDeserializer.LoadSchema(node)); + } + + public void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) + { + if (this.Schema != null) + { + if (version == AsyncApiVersion.AsyncApi2_0) this.Schema.SerializeV2(writer); + else this.Schema.SerializeV3(writer); + } + else if (this.Value != null) + { + writer.WriteValue(this.Value); + } + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs new file mode 100644 index 0000000..12647c0 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs @@ -0,0 +1,43 @@ +namespace ByteBard.AsyncAPI.Bindings.MQTT5 +{ + using System; + using ByteBard.AsyncAPI.Bindings.MQTT; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding class for MQTT 5 server settings. This binding is deprecated. + [Obsolete("The MQTT 5 server binding is deprecated.")] + public class MQTT5ServerBinding : ServerBinding + { + /// Session expiration interval in seconds, or a schema describing the value. + public MQTTIntegerOrSchema SessionExpiryInterval { get; set; } + + public override string BindingKey => "mqtt5"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "sessionExpiryInterval", (a, n) => { a.SessionExpiryInterval = MQTTIntegerOrSchema.Parse(n); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); + + public override void SerializeProperties(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); + + private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + if (this.SessionExpiryInterval != null) + { + writer.WritePropertyName("sessionExpiryInterval"); + this.SessionExpiryInterval.Serialize(writer, version); + } + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/NATS/NATSOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/NATS/NATSOperationBinding.cs new file mode 100644 index 0000000..218f894 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/NATS/NATSOperationBinding.cs @@ -0,0 +1,34 @@ +namespace ByteBard.AsyncAPI.Bindings.NATS +{ + using System; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding class for NATS operations. + public class NATSOperationBinding : OperationBinding + { + /// Queue group name. + public string Queue { get; set; } + + public override string BindingKey => "nats"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "queue", (a, n) => a.Queue = n.GetScalarValue() }, + { "bindingVersion", (a, n) => a.BindingVersion = n.GetScalarValue() }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeProperties(writer); + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("queue", this.Queue); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2OperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2OperationBinding.cs new file mode 100644 index 0000000..50f333e --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2OperationBinding.cs @@ -0,0 +1,37 @@ +namespace ByteBard.AsyncAPI.Bindings.ROS2 +{ + using System; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding class for ROS 2 operation settings. + public class ROS2OperationBinding : OperationBinding + { + public string Role { get; set; } + public string Node { get; set; } + public ROS2QosPolicies QosPolicies { get; set; } + + public override string BindingKey => "ros2"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "role", (a, n) => { a.Role = n.GetScalarValue(); } }, + { "node", (a, n) => { a.Node = n.GetScalarValue(); } }, + { "qosPolicies", (a, n) => { a.QosPolicies = n.ParseMap(ROS2QosPolicies.FixedFields); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("role", this.Role); + writer.WriteOptionalProperty("node", this.Node); + writer.WriteOptionalObject("qosPolicies", this.QosPolicies, (w, qos) => qos.Serialize(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs new file mode 100644 index 0000000..f96c9ee --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs @@ -0,0 +1,43 @@ +namespace ByteBard.AsyncAPI.Bindings.ROS2 +{ + using System; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Quality-of-service policies for a ROS 2 operation. + public class ROS2QosPolicies : IAsyncApiElement + { + public string Reliability { get; set; } + public string History { get; set; } + public string Durability { get; set; } + public int? Lifespan { get; set; } + public int? Deadline { get; set; } + public string Liveliness { get; set; } + public int? LeaseDuration { get; set; } + + internal static FixedFieldMap FixedFields => new() + { + { "reliability", (a, n) => { a.Reliability = n.GetScalarValue(); } }, + { "history", (a, n) => { a.History = n.GetScalarValue(); } }, + { "durability", (a, n) => { a.Durability = n.GetScalarValue(); } }, + { "lifespan", (a, n) => { a.Lifespan = n.GetIntegerValueOrDefault(); } }, + { "deadline", (a, n) => { a.Deadline = n.GetIntegerValueOrDefault(); } }, + { "liveliness", (a, n) => { a.Liveliness = n.GetScalarValue(); } }, + { "leaseDuration", (a, n) => { a.LeaseDuration = n.GetIntegerValueOrDefault(); } }, + }; + + internal void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("reliability", this.Reliability); + writer.WriteOptionalProperty("history", this.History); + writer.WriteOptionalProperty("durability", this.Durability); + writer.WriteOptionalProperty("lifespan", this.Lifespan); + writer.WriteOptionalProperty("deadline", this.Deadline); + writer.WriteOptionalProperty("liveliness", this.Liveliness); + writer.WriteOptionalProperty("leaseDuration", this.LeaseDuration); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs new file mode 100644 index 0000000..08c57a3 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs @@ -0,0 +1,38 @@ +namespace ByteBard.AsyncAPI.Bindings.ROS2 +{ + using System; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding class for ROS 2 server settings. + public class ROS2ServerBinding : ServerBinding + { + /// The ROS middleware implementation. + public string RmwImplementation { get; set; } + + /// The ROS domain ID. + public int? DomainId { get; set; } + + public override string BindingKey => "ros2"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "rmwImplementation", (a, n) => { a.RmwImplementation = n.GetScalarValue(); } }, + { "domainId", (a, n) => { a.DomainId = n.GetIntegerValueOrDefault(); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("rmwImplementation", this.RmwImplementation); + writer.WriteOptionalProperty("domainId", this.DomainId); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceDestination.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceDestination.cs new file mode 100644 index 0000000..19c0430 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceDestination.cs @@ -0,0 +1,38 @@ +namespace ByteBard.AsyncAPI.Bindings.Solace +{ + using System; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// A destination configured for a Solace operation. + public class SolaceDestination : IAsyncApiElement + { + public string BindingVersion { get; set; } + public string DestinationType { get; set; } + public string DeliveryMode { get; set; } + public SolaceQueue Queue { get; set; } + public SolaceTopic Topic { get; set; } + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("destinationType", this.DestinationType); + writer.WriteOptionalProperty("deliveryMode", this.DeliveryMode); + writer.WriteOptionalObject("queue", this.Queue, (w, q) => q.Serialize(w)); + writer.WriteOptionalObject("topic", this.Topic, (w, t) => t.Serialize(w)); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteEndObject(); + } + + internal static FixedFieldMap Fields => new() + { + { "bindingVersion", (a, n) => a.BindingVersion = n.GetScalarValue() }, + { "destinationType", (a, n) => a.DestinationType = n.GetScalarValue() }, + { "deliveryMode", (a, n) => a.DeliveryMode = n.GetScalarValue() }, + { "queue", (a, n) => a.Queue = n.ParseMap(SolaceQueue.Fields) }, + { "topic", (a, n) => a.Topic = n.ParseMap(SolaceTopic.Fields) }, + }; + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs new file mode 100644 index 0000000..3bbcf33 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs @@ -0,0 +1,62 @@ +namespace ByteBard.AsyncAPI.Bindings.Solace +{ + using System; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + /// Represents a Solace integer, JSON Schema object, or JSON Schema reference. + public class SolaceIntegerOrSchema : IAsyncApiSerializable + { + private long? integer; + private AsyncApiJsonSchema schema; + + public long? Integer + { + get => this.integer; + set + { + this.integer = value; + if (value.HasValue) this.schema = null; + } + } + + public AsyncApiJsonSchema Schema + { + get => this.schema; + set + { + this.schema = value; + if (value != null) this.integer = null; + } + } + + public void SerializeV2(IAsyncApiWriter writer) => this.Serialize(writer, false); + public void SerializeV3(IAsyncApiWriter writer) => this.Serialize(writer, true); + + public void Validate() + { + if (!this.Integer.HasValue && this.Schema == null) + { + throw new InvalidOperationException("A Solace integer-or-schema value must contain an integer or a schema."); + } + } + + private void Serialize(IAsyncApiWriter writer, bool v3) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + this.Validate(); + if (this.Integer.HasValue) + { + writer.WriteValue(this.Integer.Value); + return; + } + + if (this.Schema != null) + { + if (v3) this.Schema.SerializeV3(writer); + else this.Schema.SerializeV2(writer); + } + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs new file mode 100644 index 0000000..427d5f3 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs @@ -0,0 +1,63 @@ +namespace ByteBard.AsyncAPI.Bindings.Solace +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding class for Solace operations. + public class SolaceOperationBinding : OperationBinding + { + public List Destinations { get; set; } = new(); + public SolaceIntegerOrSchema TimeToLive { get; set; } + public SolaceIntegerOrSchema Priority { get; set; } + public bool? DmqEligible { get; set; } + public override string BindingKey => "solace"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => a.BindingVersion = n.GetScalarValue() }, + { "destinations", (a, n) => a.Destinations = n.CreateList(s => s.ParseMap(SolaceDestination.Fields)) }, + { "timeToLive", (a, n) => a.TimeToLive = ReadIntegerOrSchema(n) }, + { "priority", (a, n) => a.Priority = ReadIntegerOrSchema(n) }, + { "dmqEligible", (a, n) => a.DmqEligible = n.GetBooleanValue() }, + }; + + internal static SolaceIntegerOrSchema ReadIntegerOrSchema(ParseNode node) + { + if (node is ValueNode) + { + return new SolaceIntegerOrSchema { Integer = node.GetLongValue() }; + } + + return new SolaceIntegerOrSchema { Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(node) }; + } + + public override void SerializeV2(IAsyncApiWriter writer) => Serialize(writer, false); + public override void SerializeV3(IAsyncApiWriter writer) => Serialize(writer, true); + + public override void SerializeProperties(IAsyncApiWriter writer) => Serialize(writer, false); + + private void Serialize(IAsyncApiWriter writer, bool v3) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + this.TimeToLive?.Validate(); + this.Priority?.Validate(); + writer.WriteStartObject(); + writer.WriteOptionalCollection("destinations", this.Destinations, (w, d) => d.Serialize(w)); + writer.WriteOptionalObject("timeToLive", this.TimeToLive, (w, value) => { if (v3) value.SerializeV3(w); else value.SerializeV2(w); }); + writer.WriteOptionalObject("priority", this.Priority, (w, value) => { if (v3) value.SerializeV3(w); else value.SerializeV2(w); }); + if (this.DmqEligible.HasValue) + { + writer.WritePropertyName("dmqEligible"); + writer.WriteValue(this.DmqEligible.Value); + } + + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceQueue.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceQueue.cs new file mode 100644 index 0000000..e488637 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceQueue.cs @@ -0,0 +1,39 @@ +namespace ByteBard.AsyncAPI.Bindings.Solace +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Queue destination settings in a Solace operation binding. + public class SolaceQueue : IAsyncApiElement + { + public string Name { get; set; } + public List TopicSubscriptions { get; set; } = new(); + public string AccessType { get; set; } + public string MaxMsgSpoolSize { get; set; } + public string MaxTtl { get; set; } + + internal static FixedFieldMap Fields => new() + { + { "name", (a, n) => a.Name = n.GetScalarValue() }, + { "topicSubscriptions", (a, n) => a.TopicSubscriptions = n.CreateSimpleList(s => s.GetScalarValue()) }, + { "accessType", (a, n) => a.AccessType = n.GetScalarValue() }, + { "maxMsgSpoolSize", (a, n) => a.MaxMsgSpoolSize = n.GetScalarValue() }, + { "maxTtl", (a, n) => a.MaxTtl = n.GetScalarValue() }, + }; + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteOptionalCollection("topicSubscriptions", this.TopicSubscriptions, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty("accessType", this.AccessType); + writer.WriteOptionalProperty("maxMsgSpoolSize", this.MaxMsgSpoolSize); + writer.WriteOptionalProperty("maxTtl", this.MaxTtl); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceServerBinding.cs new file mode 100644 index 0000000..16cd688 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceServerBinding.cs @@ -0,0 +1,35 @@ +namespace ByteBard.AsyncAPI.Bindings.Solace +{ + using System; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Binding class for Solace servers. + public class SolaceServerBinding : ServerBinding + { + public string MsgVpn { get; set; } + public string ClientName { get; set; } + public override string BindingKey => "solace"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => a.BindingVersion = n.GetScalarValue() }, + { "msgVpn", (a, n) => a.MsgVpn = n.GetScalarValue() }, + { "clientName", (a, n) => a.ClientName = n.GetScalarValue() }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeProperties(writer); + public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalProperty("msgVpn", this.MsgVpn); + writer.WriteOptionalProperty("clientName", this.ClientName); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceTopic.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceTopic.cs new file mode 100644 index 0000000..63d18b6 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceTopic.cs @@ -0,0 +1,27 @@ +namespace ByteBard.AsyncAPI.Bindings.Solace +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Writers; + + /// Topic destination settings in a Solace operation binding. + public class SolaceTopic : IAsyncApiElement + { + public List TopicSubscriptions { get; set; } = new(); + + internal static FixedFieldMap Fields => new() + { + { "topicSubscriptions", (a, n) => a.TopicSubscriptions = n.CreateSimpleList(s => s.GetScalarValue()) }, + }; + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + writer.WriteStartObject(); + writer.WriteOptionalCollection("topicSubscriptions", this.TopicSubscriptions, (w, s) => w.WriteValue(s)); + writer.WriteEndObject(); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/AnypointMQ/AnypointMQBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/AnypointMQ/AnypointMQBindings_Should.cs new file mode 100644 index 0000000..04f2759 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/AnypointMQ/AnypointMQBindings_Should.cs @@ -0,0 +1,45 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.AnypointMQ +{ + using ByteBard.AsyncAPI.Bindings.AnypointMQ; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using FluentAssertions; + using NUnit.Framework; + + internal class AnypointMQBindings_Should : TestBase + { + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ChannelBinding_RoundTripsNestedPropertiesAndOmittedVersion(AsyncApiVersion version) + { + var original = new AsyncApiChannel(); + original.Bindings.Add(new AnypointMQChannelBinding { Destination = "orders", DestinationType = "queue" }); + + var yaml = original.SerializeAsYaml(version); + yaml.Should().Contain("destination: orders").And.Contain("destinationType: queue").And.NotContain("bindingVersion"); + var settings = new AsyncApiReaderSettings { Bindings = new[] { new AnypointMQChannelBinding() } }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out _); + + parsed.Bindings["anypointmq"].Should().BeEquivalentTo(original.Bindings["anypointmq"]); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void MessageBinding_RoundTripsHeadersSchema(AsyncApiVersion version) + { + var original = new AsyncApiMessage(); + original.Bindings.Add(new AnypointMQMessageBinding + { + BindingVersion = "0.1.0", + Headers = new AsyncApiJsonSchema { Type = SchemaType.Object, Properties = { ["traceId"] = new AsyncApiJsonSchema { Type = SchemaType.String } } }, + }); + + var yaml = original.SerializeAsYaml(version); + yaml.Should().Contain("headers:").And.Contain("traceId:").And.Contain("bindingVersion: 0.1.0"); + var settings = new AsyncApiReaderSettings { Bindings = new[] { new AnypointMQMessageBinding() } }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out _); + + parsed.Bindings["anypointmq"].Should().BeEquivalentTo(original.Bindings["anypointmq"]); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/GooglePubSub/GooglePubSubBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/GooglePubSub/GooglePubSubBindings_Should.cs new file mode 100644 index 0000000..2f26417 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/GooglePubSub/GooglePubSubBindings_Should.cs @@ -0,0 +1,54 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.GooglePubSub +{ + using System.Collections.Generic; + using FluentAssertions; + using ByteBard.AsyncAPI.Bindings; + using ByteBard.AsyncAPI.Bindings.GooglePubSub; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Readers.Interface; + using NUnit.Framework; + + internal class GooglePubSubBindings_Should : TestBase + { + private static ICollection> Parsers => new List> + { + new GooglePubSubChannelBinding(), new GooglePubSubMessageBinding(), + }; + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void GooglePubSubBindings_RoundTripShape(AsyncApiVersion version) + { + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new GooglePubSubChannelBinding + { + Labels = new Dictionary { ["team"] = "events" }, + MessageRetentionDuration = "86400s", + MessageStoragePolicy = new MessageStoragePolicy { AllowedPersistenceRegions = new[] { "us-central1", "us-east1" } }, + SchemaSettings = new SchemaSettings { Encoding = "JSON", FirstRevisionId = "1", LastRevisionId = "3", Name = "orders" }, + }); + + var channelYaml = channel.SerializeAsYaml(version); + channelYaml.Should().Contain("messageRetentionDuration: 86400s") + .And.Contain("allowedPersistenceRegions:") + .And.Contain("firstRevisionId: '1'"); + var settings = new AsyncApiReaderSettings { Bindings = Parsers }; + var parsedChannel = new AsyncApiStringReader(settings).ReadFragment(channelYaml, version, out _); + parsedChannel.Should().BeEquivalentTo(channel); + + var message = new AsyncApiMessage(); + message.Bindings.Add(new GooglePubSubMessageBinding + { + Attributes = new Dictionary { ["source"] = "billing" }, + OrderingKey = "customer-7", + Schema = new MessageSchema { Name = "projects/p/schemas/orders" }, + }); + var messageYaml = message.SerializeAsYaml(version); + messageYaml.Should().Contain("orderingKey: customer-7").And.Contain("projects/p/schemas/orders"); + var parsedMessage = new AsyncApiStringReader(settings).ReadFragment(messageYaml, version, out _); + parsedMessage.Should().BeEquivalentTo(message); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/IBMMQ/IBMMQBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/IBMMQ/IBMMQBindings_Should.cs new file mode 100644 index 0000000..3e19212 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/IBMMQ/IBMMQBindings_Should.cs @@ -0,0 +1,70 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.IBMMQ +{ + using System.Collections.Generic; + using FluentAssertions; + using ByteBard.AsyncAPI.Bindings; + using ByteBard.AsyncAPI.Bindings.IBMMQ; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Readers.Interface; + using NUnit.Framework; + + internal class IBMMQBindings_Should : TestBase + { + private static ICollection> Parsers => new List> + { + new IBMMQServerBinding(), new IBMMQChannelBinding(), new IBMMQMessageBinding(), + }; + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void IBMMQBindings_RoundTripAndOmitUnsetValues(AsyncApiVersion version) + { + var server = new AsyncApiServer { Host = "mq.example", Protocol = "ibmmq" }; + server.Bindings.Add(new IBMMQServerBinding + { + GroupId = "group-a", CcdtQueueManagerName = "QM1", CipherSpec = "TLS_AES_256_GCM_SHA384", + MultiEndpointServer = false, HeartBeatInterval = 0, + }); + var serverYaml = server.SerializeAsYaml(version); + serverYaml.Should().Contain("multiEndpointServer: false").And.Contain("heartBeatInterval: 0"); + new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) + .ReadFragment(serverYaml, version, out _).Should().BeEquivalentTo(server); + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new IBMMQChannelBinding + { + DestinationType = "queue", MaxMsgLength = 0, + Queue = new Queue { ObjectName = "ORDERS", IsPartitioned = false, Exclusive = true }, + }); + var channelYaml = channel.SerializeAsYaml(version); + channelYaml.Should().Contain("isPartitioned: false").And.Contain("maxMsgLength: 0").And.NotContain("topic:"); + new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) + .ReadFragment(channelYaml, version, out _).Should().BeEquivalentTo(channel); + + var topicChannel = new AsyncApiChannel(); + topicChannel.Bindings.Add(new IBMMQChannelBinding + { + DestinationType = "topic", + Topic = new Topic { StringValue = "orders/#", ObjectName = "ORDERS.TOPIC", DurablePermitted = false, LastMsgRetained = true }, + }); + var topicYaml = topicChannel.SerializeAsYaml(version); + topicYaml.Should().Contain("string: orders/#").And.NotContain("queue:"); + new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) + .ReadFragment(topicYaml, version, out _).Should().BeEquivalentTo(topicChannel); + + var message = new AsyncApiMessage(); + message.Bindings.Add(new IBMMQMessageBinding { Type = "binary", Headers = "STRING", Description = "event headers", Expiry = 0 }); + var messageYaml = message.SerializeAsYaml(version); + messageYaml.Should().Contain("headers: STRING").And.Contain("expiry: 0"); + new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) + .ReadFragment(messageYaml, version, out _).Should().BeEquivalentTo(message); + + var absent = new AsyncApiServer(); + absent.Bindings.Add(new IBMMQServerBinding()); + var absentYaml = absent.SerializeAsYaml(version); + absentYaml.Should().NotContain("multiEndpointServer").And.NotContain("heartBeatInterval").And.NotContain("bindingVersion"); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/JMS/JMSBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/JMS/JMSBindings_Should.cs new file mode 100644 index 0000000..6de032a --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/JMS/JMSBindings_Should.cs @@ -0,0 +1,60 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.JMS +{ + using ByteBard.AsyncAPI.Bindings.JMS; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using FluentAssertions; + using NUnit.Framework; + + internal class JMSBindings_Should : TestBase + { + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBinding_RoundTripsNestedTypedPropertiesAndOmittedVersion(AsyncApiVersion version) + { + var original = new AsyncApiServer { Host = "localhost", Protocol = "jms" }; + original.Bindings.Add(new JMSServerBinding + { + JmsConnectionFactory = "ConnectionFactory", + ClientID = "worker", + Properties = new[] + { + new JMSProperty { Name = "enabled", Value = new AsyncApiAny(false) }, + new JMSProperty { Name = "priority", Value = new AsyncApiAny(0) }, + new JMSProperty { Name = "region", Value = new AsyncApiAny("west") }, + }, + }); + + var yaml = original.SerializeAsYaml(version); + yaml.Should().Contain("jmsConnectionFactory: ConnectionFactory") + .And.Contain("clientID: worker").And.Contain("value: false").And.Contain("value: 0") + .And.NotContain("bindingVersion"); + var settings = new AsyncApiReaderSettings { Bindings = new[] { new JMSServerBinding() } }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out _); + + parsed.Bindings["jms"].Should().BeEquivalentTo(original.Bindings["jms"]); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ChannelAndMessageBindings_RoundTrip(AsyncApiVersion version) + { + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new JMSChannelBinding { Destination = "orders", DestinationType = "queue" }); + var channelYaml = channel.SerializeAsYaml(version); + var channelSettings = new AsyncApiReaderSettings { Bindings = new[] { new JMSChannelBinding() } }; + var parsedChannel = new AsyncApiStringReader(channelSettings).ReadFragment(channelYaml, version, out _); + parsedChannel.Bindings["jms"].Should().BeEquivalentTo(channel.Bindings["jms"]); + + var message = new AsyncApiMessage(); + message.Bindings.Add(new JMSMessageBinding + { + Headers = new AsyncApiJsonSchema { Type = SchemaType.Object, Properties = { ["correlationId"] = new AsyncApiJsonSchema { Type = SchemaType.String } } }, + }); + var messageYaml = message.SerializeAsYaml(version); + var messageSettings = new AsyncApiReaderSettings { Bindings = new[] { new JMSMessageBinding() } }; + var parsedMessage = new AsyncApiStringReader(messageSettings).ReadFragment(messageYaml, version, out _); + parsedMessage.Bindings["jms"].Should().BeEquivalentTo(message.Bindings["jms"]); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs index 96baa94..2d55286 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs @@ -1,6 +1,7 @@ namespace ByteBard.AsyncAPI.Tests.Bindings.Kafka { using System.Collections.Generic; + using System.Text.Json.Nodes; using FluentAssertions; using ByteBard.AsyncAPI.Bindings; using ByteBard.AsyncAPI.Bindings.Kafka; @@ -10,6 +11,39 @@ internal class KafkaBindings_Should : TestBase { + [Test] + public void V2_KafkaTopicConfiguration_PreservesAdditionalScalarAndListValues() + { + var yaml = """ + bindings: + kafka: + topicConfiguration: + custom.string: value + custom.integer: 12 + custom.boolean: false + custom.list: + - one + - 2 + - true + """; + var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Kafka }; + var reader = new AsyncApiStringReader(settings); + + var channel = reader.ReadFragment(yaml, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + diagnostic.Errors.Should().BeEmpty(); + + var serialized = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + var roundTripped = reader.ReadFragment(serialized, AsyncApiVersion.AsyncApi2_0, out diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + var configuration = ((KafkaChannelBinding)roundTripped.Bindings["kafka"]).TopicConfiguration; + configuration.AdditionalProperties["custom.string"].GetValue().Should().Be("value"); + configuration.AdditionalProperties["custom.integer"].GetValue().Should().Be(12); + configuration.AdditionalProperties["custom.boolean"].GetValue().Should().BeFalse(); + configuration.AdditionalProperties["custom.list"].GetNode().Should().BeOfType(); + configuration.AdditionalProperties["custom.list"].GetNode().ToJsonString().Should().Be("[\"one\",2,true]"); + } + [Test] public void V2_KafkaChannelBinding_WithFilledObject_SerializesAndDeserializes() { diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs new file mode 100644 index 0000000..8c3f241 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs @@ -0,0 +1,115 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.MQTT +{ + using ByteBard.AsyncAPI.Bindings; + using ByteBard.AsyncAPI.Bindings.MQTT; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using FluentAssertions; + using NUnit.Framework; + + public class MQTTBindingVariants_Should + { + [Test] + public void V2_SchemaAndReferenceVariants_AndBindingVersions_AreParsedAndSerialized() + { + var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.MQTT }; + var serverYaml = + """ + bindings: + mqtt: + sessionExpiryInterval: + type: integer + minimum: 1 + maximumPacketSize: + $ref: '#/components/schemas/PacketSize' + bindingVersion: 0.2.0 + """; + serverYaml = "url: localhost\nprotocol: mqtt\n" + serverYaml; + var server = new AsyncApiStringReader(settings).ReadFragment(serverYaml, AsyncApiVersion.AsyncApi2_0, out var serverDiagnostics); + serverDiagnostics.Errors.Should().BeEmpty(); + var serverBinding = server.Bindings["mqtt"] as MQTTServerBinding; + + serverBinding.BindingVersion.Should().Be("0.2.0"); + serverBinding.SessionExpiryIntervalValue.Schema.Type.Should().Be(SchemaType.Integer); + serverBinding.MaximumPacketSizeValue.Schema.Should().BeOfType(); + server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0).Should().Contain("minimum: 1").And.Contain("$ref: '#/components/schemas/PacketSize'"); + + var operationYaml = + """ + bindings: + mqtt: + qos: 1 + retain: false + messageExpiryInterval: + $ref: '#/components/schemas/Expiry' + bindingVersion: 0.2.0 + """; + var operation = new AsyncApiStringReader(settings).ReadFragment(operationYaml, AsyncApiVersion.AsyncApi2_0, out _); + var operationBinding = operation.Bindings["mqtt"] as MQTTOperationBinding; + operationBinding.BindingVersion.Should().Be("0.2.0"); + operationBinding.MessageExpiryIntervalValue.Schema.Should().BeOfType(); + + var messageYaml = + """ + bindings: + mqtt: + responseTopic: + type: string + pattern: '^reply/' + bindingVersion: 0.2.0 + """; + var message = new AsyncApiStringReader(settings).ReadFragment(messageYaml, AsyncApiVersion.AsyncApi2_0, out _); + var messageBinding = message.Bindings["mqtt"] as MQTTMessageBinding; + messageBinding.BindingVersion.Should().Be("0.2.0"); + messageBinding.ResponseTopicValue.Schema.Type.Should().Be(SchemaType.String); + message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0).Should().Contain("pattern: ^reply/"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerSchemaOnlyBinding_RoundTripsWithoutDiagnostics(AsyncApiVersion version) + { + var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.MQTT }; + var yaml = (version == AsyncApiVersion.AsyncApi2_0 ? "url: localhost\nprotocol: mqtt\n" : "host: localhost\nprotocol: mqtt\n") + """ + bindings: + mqtt: + sessionExpiryInterval: + type: integer + """; + + var reader = new AsyncApiStringReader(settings); + var first = reader.ReadFragment(yaml, version, out var firstDiagnostics); + firstDiagnostics.Errors.Should().BeEmpty(); + first.SerializeAsYaml(version).Should().NotContain("clientId: null"); + + var second = reader.ReadFragment(first.SerializeAsYaml(version), version, out var secondDiagnostics); + secondDiagnostics.Errors.Should().BeEmpty(); + (second.Bindings["mqtt"] as MQTTServerBinding).SessionExpiryIntervalValue.Schema.Type.Should().Be(SchemaType.Integer); + } + + [Test] + public void IntegerUnion_PreservesUnsigned32BitMaximum() + { + var binding = new MQTTServerBinding { SessionExpiryIntervalValue = MQTTIntegerOrSchema.FromLong(4294967295L) }; + var yaml = binding.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0); + + yaml.Should().Contain("sessionExpiryInterval: 4294967295"); + var parsedYaml = "host: localhost\nprotocol: mqtt\n" + """ + bindings: + mqtt: + sessionExpiryInterval: 4294967295 + """; + var parsed = new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = BindingsCollection.MQTT }) + .ReadFragment(parsedYaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics); + diagnostics.Errors.Should().BeEmpty(); + (parsed.Bindings["mqtt"] as MQTTServerBinding).SessionExpiryIntervalValue.LongInteger.Should().Be(4294967295L); + } + + [Test] + public void OperationBinding_OmitsUnsetQosAndRetain() + { + var yaml = new MQTTOperationBinding().SerializeAsYaml(AsyncApiVersion.AsyncApi3_0); + yaml.Should().NotContain("qos:").And.NotContain("retain:"); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT5/MQTT5Bindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT5/MQTT5Bindings_Should.cs new file mode 100644 index 0000000..92fff0a --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT5/MQTT5Bindings_Should.cs @@ -0,0 +1,88 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.MQTT5 +{ + using FluentAssertions; + using ByteBard.AsyncAPI.Bindings.MQTT; + using ByteBard.AsyncAPI.Bindings.MQTT5; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using NUnit.Framework; + + [System.Obsolete("The MQTT 5 server binding is deprecated.")] + internal class MQTT5Bindings_Should : TestBase + { + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBinding_SerializesScalarZeroAndMinusOne(AsyncApiVersion version) + { + var binding = new MQTT5ServerBinding + { + SessionExpiryInterval = MQTTIntegerOrSchema.FromInteger(0), + BindingVersion = "0.1.0", + }; + var actual = binding.SerializeAsYaml(version); + actual.Should().Contain("sessionExpiryInterval: 0").And.Contain("bindingVersion: 0.1.0"); + + binding.SessionExpiryInterval = MQTTIntegerOrSchema.FromInteger(-1); + actual = binding.SerializeAsYaml(version); + actual.Should().Contain("sessionExpiryInterval: -1"); + + binding.SessionExpiryInterval = MQTTIntegerOrSchema.FromLong(4294967295L); + binding.SerializeAsYaml(version).Should().Contain("sessionExpiryInterval: 4294967295"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBinding_SerializesSchemaAndReference(AsyncApiVersion version) + { + var schemaBinding = new MQTT5ServerBinding + { + SessionExpiryInterval = MQTTIntegerOrSchema.FromSchema(new AsyncApiJsonSchema { Type = SchemaType.Integer }), + }; + schemaBinding.SerializeAsYaml(version).Should().Contain("type: integer"); + + var referenceBinding = new MQTT5ServerBinding + { + SessionExpiryInterval = MQTTIntegerOrSchema.FromSchema(new AsyncApiJsonSchemaReference("#/components/schemas/interval")), + }; + referenceBinding.SerializeAsYaml(version).Should().Contain("#/components/schemas/interval"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBinding_ReadsAndRoundTripsSchemaAndReference(AsyncApiVersion version) + { + var parsers = ByteBard.AsyncAPI.Bindings.BindingsCollection.MQTT; + parsers.Add(new MQTT5ServerBinding()); + var reader = new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = parsers }); + var yaml = (version == AsyncApiVersion.AsyncApi2_0 ? "url: localhost\nprotocol: mqtt\n" : "host: localhost\nprotocol: mqtt\n") + """ + bindings: + mqtt5: + sessionExpiryInterval: + $ref: '#/components/schemas/interval' + """; + + var parsed = reader.ReadFragment(yaml, version, out var diagnostics); + diagnostics.Errors.Should().BeEmpty(); + var binding = parsed.Bindings["mqtt5"] as MQTT5ServerBinding; + binding.SessionExpiryInterval.Schema.Should().BeOfType(); + + var serialized = parsed.SerializeAsYaml(version); + serialized.Should().Contain("#/components/schemas/interval"); + var reparsed = reader.ReadFragment(serialized, version, out var roundTripDiagnostics); + roundTripDiagnostics.Errors.Should().BeEmpty(); + (reparsed.Bindings["mqtt5"] as MQTT5ServerBinding).SessionExpiryInterval.Schema.Should().BeOfType(); + + var schemaYaml = (version == AsyncApiVersion.AsyncApi2_0 ? "url: localhost\nprotocol: mqtt\n" : "host: localhost\nprotocol: mqtt\n") + """ + bindings: + mqtt5: + sessionExpiryInterval: + type: integer + minimum: 1 + """; + var schemaParsed = reader.ReadFragment(schemaYaml, version, out var schemaDiagnostics); + schemaDiagnostics.Errors.Should().BeEmpty(); + (schemaParsed.Bindings["mqtt5"] as MQTT5ServerBinding).SessionExpiryInterval.Schema.Type.Should().Be(SchemaType.Integer); + schemaParsed.SerializeAsYaml(version).Should().Contain("minimum: 1"); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/NATS/NatsBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/NATS/NatsBindings_Should.cs new file mode 100644 index 0000000..4ad8386 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/NATS/NatsBindings_Should.cs @@ -0,0 +1,31 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.NATS +{ + using System.Collections.Generic; + using ByteBard.AsyncAPI.Bindings; + using ByteBard.AsyncAPI.Bindings.NATS; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Readers.Interface; + using ByteBard.AsyncAPI.Models.Interfaces; + using NUnit.Framework; + using FluentAssertions; + + public class NatsBindings_Should + { + [Test] + public void OperationBinding_PreservesQueueAndOmittedVersion() + { + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new NATSOperationBinding { Queue = "workers" }); + + var yaml = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + yaml.Should().Contain("queue: workers"); + yaml.Should().NotContain("bindingVersion"); + + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new List> { new NATSOperationBinding() }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, AsyncApiVersion.AsyncApi2_0, out _); + parsed.Should().BeEquivalentTo(operation); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/NestedParsing/UnknownNestedField_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/NestedParsing/UnknownNestedField_Should.cs new file mode 100644 index 0000000..2f5ee71 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/NestedParsing/UnknownNestedField_Should.cs @@ -0,0 +1,38 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.NestedParsing +{ + using ByteBard.AsyncAPI.Bindings; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using FluentAssertions; + using NUnit.Framework; + + internal class UnknownNestedField_Should : TestBase + { + [TestCase(UnmappedMemberHandling.Error, 1)] + [TestCase(UnmappedMemberHandling.Ignore, 0)] + public void FollowConfiguredUnmappedMemberHandling(UnmappedMemberHandling handling, int expectedErrorCount) + { + var yaml = """ + bindings: + amqp: + is: routingKey + exchange: + name: events + unknownNestedField: ignored-value + """; + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.AMQP, + UnmappedMemberHandling = handling, + }; + + new AsyncApiStringReader(settings).ReadFragment(yaml, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().HaveCount(expectedErrorCount); + if (expectedErrorCount > 0) + { + diagnostic.Errors[0].Message.Should().Contain("unknownNestedField"); + } + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/ROS2/ROS2Bindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/ROS2/ROS2Bindings_Should.cs new file mode 100644 index 0000000..4601ed0 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/ROS2/ROS2Bindings_Should.cs @@ -0,0 +1,78 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.ROS2 +{ + using FluentAssertions; + using ByteBard.AsyncAPI.Bindings.ROS2; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using NUnit.Framework; + + internal class ROS2Bindings_Should : TestBase + { + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void OperationBinding_WritesNestedQosAndRetainsZeroAndMinusOne(AsyncApiVersion version) + { + var binding = new ROS2OperationBinding + { + Role = "publisher", + Node = "camera", + QosPolicies = new ROS2QosPolicies + { + Reliability = "reliable", + History = "keep_last", + Durability = "transient_local", + Lifespan = 0, + Deadline = -1, + Liveliness = "automatic", + LeaseDuration = 0, + }, + }; + + var operation = new AsyncApiOperation(); + operation.Channel = new AsyncApiChannelReference("#/channels/camera"); + operation.Messages.Add(new AsyncApiMessageReference("#/components/messages/camera")); + operation.Bindings.Add(binding); + var actual = operation.SerializeAsYaml(version); + actual.Should().Contain("role: publisher") + .And.Contain("qosPolicies:") + .And.Contain("history: keep_last") + .And.Contain("durability: transient_local") + .And.Contain("lifespan: 0") + .And.Contain("deadline: -1") + .And.Contain("leaseDuration: 0") + .And.NotContain("bindingVersion:"); + + var settings = new AsyncApiReaderSettings { Bindings = new[] { new ROS2OperationBinding() } }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(actual, version, out var diagnostics); + if (version == AsyncApiVersion.AsyncApi2_0) + { + diagnostics.Errors.Should().ContainSingle().Which.ToString().Should().Contain("field 'channel'"); + } + else + { + diagnostics.Errors.Should().BeEmpty(); + } + diagnostics.Warnings.Should().BeEmpty(); + parsed.Bindings["ros2"].Should().BeEquivalentTo(binding); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBinding_OmitsUnsetFieldsAndWritesExplicitZero(AsyncApiVersion version) + { + var binding = new ROS2ServerBinding { DomainId = 0 }; + var server = new AsyncApiServer { Host = "localhost", Protocol = "ros2" }; + server.Bindings.Add(binding); + var actual = server.SerializeAsYaml(version); + actual.Should().Contain("domainId: 0") + .And.NotContain("rmwImplementation:") + .And.NotContain("bindingVersion:"); + + var settings = new AsyncApiReaderSettings { Bindings = new[] { new ROS2ServerBinding() } }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(actual, version, out var diagnostics); + diagnostics.Errors.Should().BeEmpty(); + diagnostics.Warnings.Should().BeEmpty(); + parsed.Bindings["ros2"].Should().BeEquivalentTo(binding); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Registration/BindingsCollection_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Registration/BindingsCollection_Should.cs new file mode 100644 index 0000000..11c19e2 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Registration/BindingsCollection_Should.cs @@ -0,0 +1,104 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.Registration +{ + using System.Linq; + using FluentAssertions; + using ByteBard.AsyncAPI.Bindings; + using ByteBard.AsyncAPI.Bindings.NATS; + using ByteBard.AsyncAPI.Bindings.ROS2; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Readers.Interface; + using NUnit.Framework; + + internal class BindingsCollection_Should : TestBase + { + [Test] + public void All_contains_each_protocol_and_scope_once_and_matches_families() + { + var families = new[] + { + BindingsCollection.Pulsar, BindingsCollection.Kafka, BindingsCollection.Http, BindingsCollection.Websockets, + BindingsCollection.Sqs, BindingsCollection.Sns, BindingsCollection.AMQP, BindingsCollection.MQTT, + BindingsCollection.AnypointMQ, BindingsCollection.GooglePubSub, BindingsCollection.IBMMQ, BindingsCollection.JMS, + BindingsCollection.MQTT5, BindingsCollection.NATS, BindingsCollection.ROS2, BindingsCollection.Solace, + }; + var all = BindingsCollection.All.ToArray(); + + families.Should().HaveCount(16); + all.Should().HaveCount(35); + all.Should().HaveCount(families.Sum(family => family.Count)); + all.Select(parser => parser.GetType()).Should().OnlyHaveUniqueItems(); + all.GroupBy(parser => + { + var instance = (IBinding)System.Activator.CreateInstance(parser.GetType())!; + var scope = instance is IServerBinding ? "server" : instance is IChannelBinding ? "channel" : + instance is IOperationBinding ? "operation" : "message"; + return (instance.BindingKey, scope); + }).Should().OnlyContain(group => group.Count() == 1); + families.SelectMany(family => family).Select(parser => parser.GetType()) + .Should().BeEquivalentTo(all.Select(parser => parser.GetType())); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void Reader_uses_all_to_parse_field_and_new_server_bindings(AsyncApiVersion version) + { + var reader = new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = BindingsCollection.All }); + var serverAddress = version == AsyncApiVersion.AsyncApi2_0 + ? "url: broker.example\n" + : "host: broker.example\n"; + var fieldBinding = reader.ReadFragment(serverAddress + "protocol: ros2\n" + + "bindings:\n ros2:\n rmwImplementation: rmw_fastrtps_cpp\n domainId: 7\n", version, out var ros2Diagnostics); + ros2Diagnostics.Errors.Should().BeEmpty(); + ros2Diagnostics.Warnings.Should().BeEmpty(); + + var ros2 = fieldBinding.Bindings["ros2"].Should().BeOfType().Subject; + ros2.RmwImplementation.Should().Be("rmw_fastrtps_cpp"); + ros2.DomainId.Should().Be(7); + + var mqtt5Binding = reader.ReadFragment(serverAddress + "protocol: mqtt5\nbindings:\n mqtt5: {}\n", version, out var mqtt5Diagnostics); + mqtt5Diagnostics.Errors.Should().BeEmpty(); + mqtt5Diagnostics.Warnings.Should().BeEmpty(); + mqtt5Binding.Bindings.Should().ContainSingle(binding => binding.Key == "mqtt5"); + + var operationYaml = version == AsyncApiVersion.AsyncApi2_0 + ? """ + asyncapi: 2.6.0 + info: + title: Registration test + version: 1.0.0 + channels: + orders: + publish: + operationId: consumeOrders + bindings: + nats: + queue: workers + """ + : """ + asyncapi: 3.0.0 + info: + title: Registration test + version: 1.0.0 + channels: + orders: + address: orders + operations: + consumeOrders: + action: send + channel: + $ref: '#/channels/orders' + bindings: + nats: + queue: workers + """; + var operationDocument = reader.Read(operationYaml, out var operationDiagnostics); + operationDiagnostics.Errors.Should().BeEmpty(); + operationDiagnostics.Warnings.Should().BeEmpty(); + operationDocument.Channels.Should().ContainKey("orders"); + operationDocument.Operations["consumeOrders"].Bindings["nats"].Should().BeOfType() + .Which.Queue.Should().Be("workers"); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Solace/SolaceBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Solace/SolaceBindings_Should.cs new file mode 100644 index 0000000..342b08d --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Solace/SolaceBindings_Should.cs @@ -0,0 +1,199 @@ +namespace ByteBard.AsyncAPI.Tests.Bindings.Solace +{ + using System.Collections.Generic; + using ByteBard.AsyncAPI.Bindings; + using ByteBard.AsyncAPI.Bindings.Solace; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Readers.Interface; + using ByteBard.AsyncAPI.Models.Interfaces; + using NUnit.Framework; + using FluentAssertions; + + public class SolaceBindings_Should + { + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void OperationBinding_RoundTripsNestedDestinationsAndScalarUnionValues(AsyncApiVersion version) + { + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new SolaceOperationBinding + { + DmqEligible = false, + TimeToLive = new SolaceIntegerOrSchema { Integer = 0 }, + Priority = new SolaceIntegerOrSchema { Integer = 7 }, + Destinations = + { + new SolaceDestination + { + DestinationType = "queue", + DeliveryMode = "persistent", + Queue = new SolaceQueue + { + Name = "orders", + AccessType = "exclusive", + MaxMsgSpoolSize = "100", + MaxTtl = "3600", + TopicSubscriptions = { "orders/>" }, + }, + }, + }, + }); + + var yaml = operation.SerializeAsYaml(version); + yaml.Should().Contain("dmqEligible: false").And.Contain("timeToLive: 0").And.Contain("maxTtl:"); + + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new List> { new SolaceOperationBinding() }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out _); + parsed.Should().BeEquivalentTo(operation); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void OperationBinding_RoundTripsSchemaAndReferenceUnionValues(AsyncApiVersion version) + { + var operation = new AsyncApiOperation + { + Action = AsyncApiAction.Send, + Channel = new AsyncApiChannelReference("#/channels/orders"), + }; + operation.Bindings.Add(new SolaceOperationBinding + { + TimeToLive = new SolaceIntegerOrSchema + { + Schema = new AsyncApiJsonSchema { Description = "ttl schema" }, + }, + Priority = new SolaceIntegerOrSchema + { + Schema = new AsyncApiJsonSchemaReference("#/components/schemas/Priority"), + }, + }); + + var document = new AsyncApiDocument + { + Info = new AsyncApiInfo { Title = "Solace union fixture", Version = "1.0.0" }, + }; + document.Channels.Add("orders", new AsyncApiChannel { Address = "orders" }); + document.Operations.Add("solaceOperation", operation); + document.Components.Schemas.Add("Priority", new AsyncApiMultiFormatSchema + { + Schema = new AsyncApiJsonSchema { Type = SchemaType.Integer }, + }); + + var yaml = document.SerializeAsYaml(version); + yaml.Should().Contain("description: ttl schema").And.Contain("#/components/schemas/Priority"); + + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new List> { new SolaceOperationBinding() }; + var reader = new AsyncApiStringReader(settings); + var parsed = reader.Read(yaml, out var diagnostics); + diagnostics.Errors.Should().BeEmpty(); + parsed.Operations.Should().ContainSingle(); + + var rewritten = parsed.SerializeAsYaml(version); + var reparsed = reader.Read(rewritten, out diagnostics); + diagnostics.Errors.Should().BeEmpty(); + var binding = reparsed.Operations.Values.Should().ContainSingle().Which.Bindings.Values.Should() + .ContainSingle().Which.Should().BeOfType().Subject; + binding.TimeToLive.Schema.Description.Should().Be("ttl schema"); + ((AsyncApiJsonSchemaReference)binding.Priority.Schema).Reference.Reference.Should().Be("#/components/schemas/Priority"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void OperationBinding_RoundTripsLargeTimeToLive(AsyncApiVersion version) + { + const long largeTtl = 2592000000L; + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new SolaceOperationBinding + { + TimeToLive = new SolaceIntegerOrSchema { Integer = largeTtl }, + }); + + var yaml = operation.SerializeAsYaml(version); + yaml.Should().Contain("timeToLive: 2592000000"); + + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new List> { new SolaceOperationBinding() }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out _); + var binding = parsed.Bindings.Should().ContainSingle().Which.Value.Should().BeOfType().Subject; + binding.TimeToLive.Integer.Should().Be(largeTtl); + } + + [Test] + public void OperationBinding_RejectsEmptyIntegerOrSchemaBeforeWriting() + { + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new SolaceOperationBinding + { + TimeToLive = new SolaceIntegerOrSchema(), + }); + + var exception = FluentActions.Invoking(() => operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0)) + .Should().Throw() + .Which; + exception.Message.Should().Contain("must contain an integer or a schema"); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void OperationBinding_ReportsIntegerOutsideInt64AsReaderDiagnostic(AsyncApiVersion version) + { + const string yaml = "bindings:\n solace:\n timeToLive: 9223372036854775808\n"; + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new List> { new SolaceOperationBinding() }; + + new AsyncApiStringReader(settings) + .ReadFragment(yaml, version, out var diagnostics); + diagnostics.Errors.Should().NotBeEmpty(); + diagnostics.Errors.Should().Contain(error => error.Message.Contains("could not parse to long")); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBinding_RoundTripsFields(AsyncApiVersion version) + { + var yaml = version == AsyncApiVersion.AsyncApi2_0 + ? "url: localhost\nprotocol: solace\nbindings:\n solace:\n msgVpn: orders\n clientName: consumer\n" + : "host: localhost\nprotocol: solace\nbindings:\n solace:\n msgVpn: orders\n clientName: consumer\n"; + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new List> { new SolaceServerBinding() }; + var reader = new AsyncApiStringReader(settings); + var parsed = reader.ReadFragment(yaml, version, out _); + var rewritten = parsed.SerializeAsYaml(version); + var reparsed = reader.ReadFragment(rewritten, version, out _); + reparsed.Bindings.Should().BeEquivalentTo(parsed.Bindings); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ParsedOperationBinding_RoundTripsNestedQueueAndTopicDestinations(AsyncApiVersion version) + { + const string yaml = """ + bindings: + solace: + destinations: + - destinationType: queue + deliveryMode: direct + queue: + name: orders + topicSubscriptions: [orders/>] + accessType: nonexclusive + maxMsgSpoolSize: 500 + maxTtl: 900 + - destinationType: topic + topic: + topicSubscriptions: [audit/>] + """; + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new List> { new SolaceOperationBinding() }; + var reader = new AsyncApiStringReader(settings); + var parsed = reader.ReadFragment(yaml, version, out _); + var rewritten = parsed.SerializeAsYaml(version); + var reparsed = reader.ReadFragment(rewritten, version, out _); + + reparsed.Should().BeEquivalentTo(parsed); + } + } +} From 373e714c8dc5f2a7499c7f4c13a24104f1be096d Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:05:20 +0000 Subject: [PATCH 2/3] docs: add XML documentation for binding serialization, parsing, and validation methods --- .../AnypointMQ/AnypointMQChannelBinding.cs | 13 +++++ .../AnypointMQ/AnypointMQMessageBinding.cs | 13 +++++ .../BindingHelpers.cs | 8 +++ .../BindingsCollection.cs | 7 +++ .../GooglePubSubChannelBinding.cs | 13 +++++ .../GooglePubSubMessageBinding.cs | 13 +++++ .../GooglePubSub/MessageSchema.cs | 4 ++ .../GooglePubSub/MessageStoragePolicy.cs | 4 ++ .../GooglePubSub/SchemaSettings.cs | 4 ++ .../IBMMQ/IBMMQChannelBinding.cs | 13 +++++ .../IBMMQ/IBMMQMessageBinding.cs | 13 +++++ .../IBMMQ/IBMMQServerBinding.cs | 13 +++++ src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs | 4 ++ src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs | 4 ++ .../JMS/JMSChannelBinding.cs | 13 +++++ .../JMS/JMSMessageBinding.cs | 13 +++++ .../JMS/JMSProperty.cs | 4 ++ .../JMS/JMSServerBinding.cs | 13 +++++ .../Kafka/KafkaChannelBinding.cs | 6 +++ .../Kafka/TopicConfigurationObject.cs | 5 ++ .../MQTT/MQTTMessageBinding.cs | 19 +++++++ .../MQTT/MQTTOperationBinding.cs | 19 +++++++ .../MQTT/MQTTServerBinding.cs | 27 ++++++++++ .../MQTT/MQTTValueUnions.cs | 53 +++++++++++++++++++ .../MQTT5/MQTT5ServerBinding.cs | 19 +++++++ .../NATS/NATSOperationBinding.cs | 13 +++++ .../ROS2/ROS2OperationBinding.cs | 13 +++++ .../ROS2/ROS2QosPolicies.cs | 4 ++ .../ROS2/ROS2ServerBinding.cs | 13 +++++ .../Solace/SolaceDestination.cs | 4 ++ .../Solace/SolaceIntegerOrSchema.cs | 21 ++++++++ .../Solace/SolaceOperationBinding.cs | 30 +++++++++++ .../Solace/SolaceQueue.cs | 4 ++ .../Solace/SolaceServerBinding.cs | 13 +++++ .../Solace/SolaceTopic.cs | 4 ++ 35 files changed, 436 insertions(+) diff --git a/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs index eb22c6f..7c76aa0 100644 --- a/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs @@ -21,10 +21,23 @@ public class AnypointMQChannelBinding : ChannelBinding { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, }; + /// + /// Writes the Anypoint MQ channel binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the Anypoint MQ channel binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the Anypoint MQ channel binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQMessageBinding.cs index 1c3395d..82cf7eb 100644 --- a/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQMessageBinding.cs @@ -20,6 +20,10 @@ public class AnypointMQMessageBinding : MessageBinding { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, }; + /// + /// Writes the Anypoint MQ message binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); @@ -30,8 +34,17 @@ public override void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + /// + /// Writes the Anypoint MQ message binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the Anypoint MQ message binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs b/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs index 3d83116..acff5b1 100644 --- a/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs +++ b/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs @@ -8,6 +8,14 @@ namespace ByteBard.AsyncAPI.Bindings public static class BindingHelpers { + /// + /// Parses a nested object using the supplied field handlers. + /// + /// The nested object to parse. + /// Handlers for recognized property names; other properties follow the reader's unmapped-member setting. + /// A new instance populated from recognized fields, which may be incomplete when field parsing reports errors. + /// Field-level reader and AsyncAPI exceptions are recorded as diagnostics; other handler exceptions propagate. + /// is not a map. public static T ParseMap(this ParseNode node, FixedFieldMap fixedFieldMap) where T : new() { diff --git a/src/ByteBard.AsyncAPI.Bindings/BindingsCollection.cs b/src/ByteBard.AsyncAPI.Bindings/BindingsCollection.cs index 2d291a4..4c5ff39 100644 --- a/src/ByteBard.AsyncAPI.Bindings/BindingsCollection.cs +++ b/src/ByteBard.AsyncAPI.Bindings/BindingsCollection.cs @@ -23,6 +23,13 @@ namespace ByteBard.AsyncAPI.Bindings public static class BindingsCollection { + /// + /// Adds the source items to the destination collection. + /// + /// The same destination collection, after adding the items. + /// Enumeration and collection errors propagate; items already added are not rolled back. + /// or is null. + /// The destination collection does not support adding items. public static TCollection Add(this TCollection destination, IEnumerable source) where TCollection : ICollection { diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubChannelBinding.cs index 3f1f3e1..d24a8ea 100644 --- a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubChannelBinding.cs @@ -36,9 +36,22 @@ public class GooglePubSubChannelBinding : ChannelBinding { a.Name = n.GetScalarValue(); } }, }; + /// + /// Writes the Google Pub/Sub channel binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the Google Pub/Sub channel binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the Google Pub/Sub channel binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubMessageBinding.cs index 20774af..b934ace 100644 --- a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubMessageBinding.cs @@ -26,9 +26,22 @@ public class GooglePubSubMessageBinding : MessageBinding { a.Name = n.GetScalarValue(); } }, }; + /// + /// Writes the Google Pub/Sub message binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the Google Pub/Sub message binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the Google Pub/Sub message binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageSchema.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageSchema.cs index e31c71e..a31a0bd 100644 --- a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageSchema.cs +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageSchema.cs @@ -7,6 +7,10 @@ public class MessageSchema : IAsyncApiElement { public string Name { get; set; } + /// + /// Writes the message schema object, omitting an unset name. + /// + /// is null. public void Serialize(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageStoragePolicy.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageStoragePolicy.cs index 73f8462..c5f8d42 100644 --- a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageStoragePolicy.cs +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageStoragePolicy.cs @@ -8,6 +8,10 @@ public class MessageStoragePolicy : IAsyncApiElement { public IEnumerable AllowedPersistenceRegions { get; set; } + /// + /// Writes the message storage policy object, omitting null or empty persistence regions. + /// + /// is null. public void Serialize(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/SchemaSettings.cs b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/SchemaSettings.cs index 8516cac..9b6a955 100644 --- a/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/SchemaSettings.cs +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/SchemaSettings.cs @@ -10,6 +10,10 @@ public class SchemaSettings : IAsyncApiElement public string LastRevisionId { get; set; } public string Name { get; set; } + /// + /// Writes the schema settings object, omitting unset values. + /// + /// is null. public void Serialize(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQChannelBinding.cs index 9bdc2b5..8608aee 100644 --- a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQChannelBinding.cs @@ -37,9 +37,22 @@ public class IBMMQChannelBinding : ChannelBinding { "lastMsgRetained", (a, n) => { a.LastMsgRetained = n.GetBooleanValue(); } }, }; + /// + /// Writes the IBM MQ channel binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the IBM MQ channel binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the IBM MQ channel binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs index bbffc24..d4fd824 100644 --- a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs @@ -22,9 +22,22 @@ public class IBMMQMessageBinding : MessageBinding { "expiry", (a, n) => { a.Expiry = n.GetIntegerValue(); } }, }; + /// + /// Writes the IBM MQ message binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the IBM MQ message binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the IBM MQ message binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs index cff2537..8f02f8c 100644 --- a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs @@ -24,9 +24,22 @@ public class IBMMQServerBinding : ServerBinding { "heartBeatInterval", (a, n) => { a.HeartBeatInterval = n.GetIntegerValue(); } }, }; + /// + /// Writes the IBM MQ server binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the IBM MQ server binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the IBM MQ server binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs index 9b2f5d4..eab7336 100644 --- a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs @@ -9,6 +9,10 @@ public class Queue : IAsyncApiElement public bool? IsPartitioned { get; set; } public bool? Exclusive { get; set; } + /// + /// Writes the IBM MQ queue settings object, omitting unset values. + /// + /// is null. public void Serialize(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs index 2c6bb46..9c766e8 100644 --- a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs @@ -10,6 +10,10 @@ public class Topic : IAsyncApiElement public bool? DurablePermitted { get; set; } public bool? LastMsgRetained { get; set; } + /// + /// Writes the IBM MQ topic settings object, omitting unset values. + /// + /// is null. public void Serialize(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSChannelBinding.cs index d80967a..2a53bca 100644 --- a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSChannelBinding.cs @@ -21,10 +21,23 @@ public class JMSChannelBinding : ChannelBinding { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, }; + /// + /// Writes the JMS channel binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the JMS channel binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the JMS channel binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSMessageBinding.cs index a95b558..fd307ec 100644 --- a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSMessageBinding.cs @@ -20,6 +20,10 @@ public class JMSMessageBinding : MessageBinding { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, }; + /// + /// Writes the JMS message binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); @@ -30,8 +34,17 @@ public override void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + /// + /// Writes the JMS message binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the JMS message binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSProperty.cs b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSProperty.cs index b9872d8..5709695 100644 --- a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSProperty.cs +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSProperty.cs @@ -11,6 +11,10 @@ public class JMSProperty : IAsyncApiElement public AsyncApiAny Value { get; set; } + /// + /// Writes the connection property object, preserving the JSON type of its value and omitting unset fields. + /// + /// is null. public void Serialize(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSServerBinding.cs index 198cc13..a9efc18 100644 --- a/src/ByteBard.AsyncAPI.Bindings/JMS/JMSServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSServerBinding.cs @@ -31,10 +31,23 @@ public class JMSServerBinding : ServerBinding public override string BindingKey => "jms"; + /// + /// Writes the JMS server binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the JMS server binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the JMS server binding object and its extensions for AsyncAPI 3.0. + /// + /// The connection factory is written even when null; an unset binding version and empty properties collection are omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs index aa999ba..7fed53b 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs @@ -55,6 +55,12 @@ public class KafkaChannelBinding : ChannelBinding { "confluent.value.subject.name.strategy", (a, n) => { a.ConfluentValueSubjectName = n.GetScalarValue(); } }, }; + /// + /// Parses known topic settings and retains other settings as JSON values in AdditionalProperties. + /// + /// A new topic configuration; known fields with parsing errors may remain unset. + /// Reader and AsyncAPI exceptions from known field handlers are recorded as diagnostics. + /// is not a map. private static TopicConfigurationObject ParseTopicConfiguration(ParseNode node) { var mapNode = node.CheckMapNode(node.Context.GetLocation()); diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs index 3a9d410..6b8a196 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs @@ -72,6 +72,11 @@ public class TopicConfigurationObject : IAsyncApiElement /// public Dictionary AdditionalProperties { get; set; } = new(); + /// + /// Writes the topic configuration object, including additional settings whose names do not match fixed fields. + /// + /// Additional settings with fixed field names are skipped even when the corresponding fixed value is unset. + /// is null. public void Serialize(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs index bcb72a3..93214f6 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -40,21 +40,40 @@ public string ResponseTopic /// The string-or-schema response topic value. public MQTTStringOrSchema ResponseTopicValue { get; set; } + /// + /// Writes the MQTT message binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) { this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); } + /// + /// Writes the MQTT message binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) { this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); } + /// + /// Writes the MQTT message binding object and its extensions for AsyncAPI 3.0. + /// + /// ResponseTopicValue is written as a string or schema; nested schemas use AsyncAPI 3.0 serialization. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); } + /// + /// Writes the MQTT message binding object and extensions, using the selected version for nested schemas. + /// + /// The writer receiving the value. + /// Uses AsyncAPI 2.0 schema serialization for ; otherwise uses AsyncAPI 3.0. + /// is null. private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs index 7659b21..193f4e6 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs @@ -54,21 +54,40 @@ public int? MessageExpiryInterval { "messageExpiryInterval", (a, n) => { a.MessageExpiryIntervalValue = MQTTIntegerOrSchema.Parse(n); } }, }; + /// + /// Writes the MQTT operation binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) { this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); } + /// + /// Writes the MQTT operation binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) { this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); } + /// + /// Writes the MQTT operation binding object and its extensions for AsyncAPI 3.0. + /// + /// Unset QoS and retain values are omitted. Message expiry is written as an integer or schema. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); } + /// + /// Writes the MQTT operation binding object and extensions, using the selected version for nested schemas. + /// + /// The writer receiving the value. + /// Uses AsyncAPI 2.0 schema serialization for ; otherwise uses AsyncAPI 3.0. + /// is null. private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs index b7b2302..0876fa9 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs @@ -80,21 +80,40 @@ public int? MaximumPacketSize { "retain", (a, n) => { a.Retain = n.GetBooleanValue(); } }, }; + /// + /// Writes the MQTT server binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) { this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); } + /// + /// Writes the MQTT server binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) { this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); } + /// + /// Writes the MQTT server binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset client ID is omitted. Session expiry and maximum packet size are written as integers or schemas. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); } + /// + /// Writes the MQTT server binding object and extensions, using the selected version for nested schemas. + /// + /// The writer receiving the value. + /// Uses AsyncAPI 2.0 schema serialization for ; otherwise uses AsyncAPI 3.0. + /// is null. private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version) { if (writer is null) @@ -114,6 +133,14 @@ private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version writer.WriteEndObject(); } + /// + /// Writes a named integer or schema property, omitting it when the value is null. + /// + /// The writer receiving the value. + /// The property name to write. + /// The integer or schema to write, or null to omit the property. + /// Uses AsyncAPI 2.0 schema serialization for ; otherwise uses AsyncAPI 3.0. + /// A value is present and or is null. private void WriteIntegerOrSchema(IAsyncApiWriter writer, string name, MQTTIntegerOrSchema value, AsyncApiVersion version) { if (value?.LongInteger is long integer) writer.WriteRequiredProperty(name, integer); diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs index b10fa18..71f809b 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs @@ -9,6 +9,9 @@ namespace ByteBard.AsyncAPI.Bindings.MQTT /// A MQTT binding value represented by either an integer or a JSON Schema. public sealed class MQTTIntegerOrSchema { + /// + /// Stores the supplied integer and schema without validating that exactly one is present. + /// private MQTTIntegerOrSchema(long? integer, AsyncApiJsonSchema schema) { this.LongInteger = integer; @@ -25,12 +28,30 @@ private MQTTIntegerOrSchema(long? integer, AsyncApiJsonSchema schema) public AsyncApiJsonSchema Schema { get; } + /// + /// Creates an integer alternative without enforcing MQTT field limits. + /// public static MQTTIntegerOrSchema FromInteger(int value) => new(value, null); + /// + /// Creates an integer alternative without enforcing MQTT field limits. + /// + /// Values outside the Int32 range remain available through LongInteger; Integer returns null for them. public static MQTTIntegerOrSchema FromLong(long value) => new(value, null); + /// + /// Creates a schema alternative, retaining the supplied schema or reference without copying it. + /// + /// is null. public static MQTTIntegerOrSchema FromSchema(AsyncApiJsonSchema schema) => new(null, schema ?? throw new ArgumentNullException(nameof(schema))); + /// + /// Parses a scalar as a signed 64-bit integer, or an object as a schema or schema reference. + /// + /// A value containing the parsed scalar or schema. + /// Schema field errors handled by the reader are recorded as diagnostics. + /// The scalar cannot be parsed as an Int64, or a non-scalar node is not a schema object. + /// A schema reference has a non-scalar $ref value. public static MQTTIntegerOrSchema Parse(ParseNode node) { return node is ValueNode @@ -38,6 +59,13 @@ public static MQTTIntegerOrSchema Parse(ParseNode node) : FromSchema(AsyncApiJsonSchemaDeserializer.LoadSchema(node)); } + /// + /// Writes the scalar value or schema directly, without a property name or wrapper object. + /// + /// The writer receiving the value. + /// Uses AsyncAPI 2.0 schema serialization for ; otherwise uses AsyncAPI 3.0. + /// The writer is null when writing a scalar or an ordinary schema. + /// The writer is null when writing a schema reference. public void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) { if (this.LongInteger.HasValue) @@ -55,6 +83,9 @@ public void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) /// A MQTT message response topic represented by either a string or a JSON Schema. public sealed class MQTTStringOrSchema { + /// + /// Stores the supplied response topic and schema without validating that exactly one is present. + /// private MQTTStringOrSchema(string value, AsyncApiJsonSchema schema) { this.Value = value; @@ -65,10 +96,25 @@ private MQTTStringOrSchema(string value, AsyncApiJsonSchema schema) public AsyncApiJsonSchema Schema { get; } + /// + /// Creates a response topic alternative without validating topic syntax. + /// + /// is null. public static MQTTStringOrSchema FromString(string value) => new(value ?? throw new ArgumentNullException(nameof(value)), null); + /// + /// Creates a schema alternative, retaining the supplied schema or reference without copying it. + /// + /// is null. public static MQTTStringOrSchema FromSchema(AsyncApiJsonSchema schema) => new(null, schema ?? throw new ArgumentNullException(nameof(schema))); + /// + /// Converts a scalar to a response topic string, or parses an object as a schema or schema reference. + /// + /// A value containing the parsed scalar or schema. + /// Schema field errors handled by the reader are recorded as diagnostics. + /// A non-scalar node is not a schema object. + /// A schema reference has a non-scalar $ref value. public static MQTTStringOrSchema Parse(ParseNode node) { return node is ValueNode @@ -76,6 +122,13 @@ public static MQTTStringOrSchema Parse(ParseNode node) : FromSchema(AsyncApiJsonSchemaDeserializer.LoadSchema(node)); } + /// + /// Writes the scalar value or schema directly, without a property name or wrapper object. + /// + /// The writer receiving the value. + /// Uses AsyncAPI 2.0 schema serialization for ; otherwise uses AsyncAPI 3.0. + /// The writer is null when writing a scalar or an ordinary schema. + /// The writer is null when writing a schema reference. public void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) { if (this.Schema != null) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs index 12647c0..4a50df2 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs @@ -21,11 +21,30 @@ public class MQTT5ServerBinding : ServerBinding { "sessionExpiryInterval", (a, n) => { a.SessionExpiryInterval = MQTTIntegerOrSchema.Parse(n); } }, }; + /// + /// Writes the MQTT 5 server binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); + /// + /// Writes the MQTT 5 server binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); + /// + /// Writes the MQTT 5 server binding object and its extensions for AsyncAPI 2.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); + /// + /// Writes the MQTT 5 server binding object and extensions, using the selected version for nested schemas. + /// + /// The writer receiving the value. + /// Uses AsyncAPI 2.0 schema serialization for ; otherwise uses AsyncAPI 3.0. + /// is null. private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/NATS/NATSOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/NATS/NATSOperationBinding.cs index 218f894..0f59ff0 100644 --- a/src/ByteBard.AsyncAPI.Bindings/NATS/NATSOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/NATS/NATSOperationBinding.cs @@ -18,9 +18,22 @@ public class NATSOperationBinding : OperationBinding { "bindingVersion", (a, n) => a.BindingVersion = n.GetScalarValue() }, }; + /// + /// Writes the NATS operation binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the NATS operation binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the NATS operation binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2OperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2OperationBinding.cs index 50f333e..8cddc9c 100644 --- a/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2OperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2OperationBinding.cs @@ -20,9 +20,22 @@ public class ROS2OperationBinding : OperationBinding { "qosPolicies", (a, n) => { a.QosPolicies = n.ParseMap(ROS2QosPolicies.FixedFields); } }, }; + /// + /// Writes the ROS 2 operation binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the ROS 2 operation binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the ROS 2 operation binding object and its extensions for AsyncAPI 3.0. + /// + /// BindingVersion is not written. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs index f96c9ee..bd33c46 100644 --- a/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs +++ b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs @@ -27,6 +27,10 @@ public class ROS2QosPolicies : IAsyncApiElement { "leaseDuration", (a, n) => { a.LeaseDuration = n.GetIntegerValueOrDefault(); } }, }; + /// + /// Writes the QoS policy object, omitting unset values and preserving zero and negative numeric values. + /// + /// is null. internal void Serialize(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs index 08c57a3..9512645 100644 --- a/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs @@ -21,10 +21,23 @@ public class ROS2ServerBinding : ServerBinding { "domainId", (a, n) => { a.DomainId = n.GetIntegerValueOrDefault(); } }, }; + /// + /// Writes the ROS 2 server binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); + /// + /// Writes the ROS 2 server binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the ROS 2 server binding object and its extensions for AsyncAPI 3.0. + /// + /// BindingVersion is not written. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceDestination.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceDestination.cs index 19c0430..896c00f 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceDestination.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceDestination.cs @@ -14,6 +14,10 @@ public class SolaceDestination : IAsyncApiElement public SolaceQueue Queue { get; set; } public SolaceTopic Topic { get; set; } + /// + /// Writes the destination object with any queue, topic, and binding version settings. + /// + /// is null. public void Serialize(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs index 3bbcf33..ee4b18d 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs @@ -31,9 +31,23 @@ public AsyncApiJsonSchema Schema } } + /// + /// Writes the integer or schema value directly for AsyncAPI 2.0, after validating that a value is present. + /// + /// is null. + /// Neither an integer nor a schema is set. public void SerializeV2(IAsyncApiWriter writer) => this.Serialize(writer, false); + /// + /// Writes the integer or schema value directly for AsyncAPI 3.0, after validating that a value is present. + /// + /// is null. + /// Neither an integer nor a schema is set. public void SerializeV3(IAsyncApiWriter writer) => this.Serialize(writer, true); + /// + /// Checks that an integer or schema is present, without validating its range or schema contents. + /// + /// Neither an integer nor a schema is set. public void Validate() { if (!this.Integer.HasValue && this.Schema == null) @@ -42,6 +56,13 @@ public void Validate() } } + /// + /// Validates and writes the integer or schema value without a property name or wrapper object. + /// + /// The writer receiving the value. + /// Uses AsyncAPI 3.0 schema serialization when true and AsyncAPI 2.0 when false. + /// is null. + /// Neither an integer nor a schema is set. private void Serialize(IAsyncApiWriter writer, bool v3) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs index 427d5f3..4b3dc48 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs @@ -25,6 +25,13 @@ public class SolaceOperationBinding : OperationBinding { "dmqEligible", (a, n) => a.DmqEligible = n.GetBooleanValue() }, }; + /// + /// Parses a scalar as a signed 64-bit integer, or an object as a schema or schema reference. + /// + /// A value containing the parsed integer or schema. + /// Schema field errors handled by the reader are recorded as diagnostics. Scalar parsing errors propagate to the caller. + /// The scalar cannot be parsed as an Int64, or a non-scalar node is not a schema object. + /// A schema reference has a non-scalar $ref value. internal static SolaceIntegerOrSchema ReadIntegerOrSchema(ParseNode node) { if (node is ValueNode) @@ -35,11 +42,34 @@ internal static SolaceIntegerOrSchema ReadIntegerOrSchema(ParseNode node) return new SolaceIntegerOrSchema { Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(node) }; } + /// + /// Writes the Solace operation binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. + /// TimeToLive or Priority is present but contains neither an integer nor a schema. public override void SerializeV2(IAsyncApiWriter writer) => Serialize(writer, false); + /// + /// Writes the Solace operation binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. + /// TimeToLive or Priority is present but contains neither an integer nor a schema. public override void SerializeV3(IAsyncApiWriter writer) => Serialize(writer, true); + /// + /// Writes the Solace operation binding object and its extensions for AsyncAPI 2.0. + /// + /// Validates the optional integer-or-schema values before writing the binding. Explicit false DmqEligible values are preserved. + /// is null. + /// TimeToLive or Priority is present but contains neither an integer nor a schema. public override void SerializeProperties(IAsyncApiWriter writer) => Serialize(writer, false); + /// + /// Validates the optional integer-or-schema values, then writes the binding object and its extensions. + /// + /// The writer receiving the value. + /// Uses AsyncAPI 3.0 schema serialization when true and AsyncAPI 2.0 when false. + /// is null. + /// TimeToLive or Priority is present but contains neither an integer nor a schema. private void Serialize(IAsyncApiWriter writer, bool v3) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceQueue.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceQueue.cs index e488637..ae1d61c 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceQueue.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceQueue.cs @@ -24,6 +24,10 @@ public class SolaceQueue : IAsyncApiElement { "maxTtl", (a, n) => a.MaxTtl = n.GetScalarValue() }, }; + /// + /// Writes the queue settings object, omitting unset values and empty topic subscriptions. + /// + /// is null. public void Serialize(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceServerBinding.cs index 16cd688..d7756d1 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceServerBinding.cs @@ -18,9 +18,22 @@ public class SolaceServerBinding : ServerBinding { "clientName", (a, n) => a.ClientName = n.GetScalarValue() }, }; + /// + /// Writes the Solace server binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the Solace server binding object and its extensions for AsyncAPI 3.0. + /// + /// is null. public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer); + /// + /// Writes the Solace server binding object and its extensions for AsyncAPI 3.0. + /// + /// An unset binding version is omitted. + /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); diff --git a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceTopic.cs b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceTopic.cs index 63d18b6..f923b81 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceTopic.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceTopic.cs @@ -16,6 +16,10 @@ public class SolaceTopic : IAsyncApiElement { "topicSubscriptions", (a, n) => a.TopicSubscriptions = n.CreateSimpleList(s => s.GetScalarValue()) }, }; + /// + /// Writes the topic settings object, omitting null or empty topic subscriptions. + /// + /// is null. public void Serialize(IAsyncApiWriter writer) { if (writer is null) throw new ArgumentNullException(nameof(writer)); From d53557a1734dcff3179f6cda16e384bd949b2467 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 12 Oct 2026 01:09:47 +0200 Subject: [PATCH 3/3] refactor: Update binding serialization and testing for various protocols This commit updates the serialization logic across several bindings and corresponding test cases to align with newer API versions and improve data handling. In src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs, the `Expiry` property type is updated from `int?` to `long?`, and the mapping in `FixedFieldMap` is adjusted to use `GetLongValue()`. In src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs, the serialization logic for `multiEndpointServer` is updated to use the strongly typed property `bool`. In src/ByteBard.AsyncAPI.Bindings/Queue.cs, the serialization of `isPartitioned` and `exclusive` properties is updated to use the strongly typed `bool` type. In src/ByteBard.AsyncAPI.Bindings/Topic.cs, the serialization of `durablePermitted` and `lastMsgRetained` properties is updated to use the strongly typed `bool` type. In src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs, the serialization logic for `SerializeProperties --- .../IBMMQ/IBMMQMessageBinding.cs | 4 +-- .../IBMMQ/IBMMQServerBinding.cs | 2 +- src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs | 4 +-- src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs | 4 +-- .../MQTT5/MQTT5ServerBinding.cs | 2 +- .../AnypointMQ/AnypointMQBindings_Should.cs | 6 ++-- .../GooglePubSubBindings_Should.cs | 6 ++-- .../Bindings/IBMMQ/IBMMQBindings_Should.cs | 33 +++++++++++++---- .../Bindings/ROS2/ROS2Bindings_Should.cs | 36 +++++++++++++++++++ 9 files changed, 79 insertions(+), 18 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs index bbffc24..7c4581b 100644 --- a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs @@ -9,7 +9,7 @@ public class IBMMQMessageBinding : MessageBinding public string Type { get; set; } public string Headers { get; set; } public string Description { get; set; } - public int? Expiry { get; set; } + public long? Expiry { get; set; } public override string BindingKey => "ibmmq"; @@ -19,7 +19,7 @@ public class IBMMQMessageBinding : MessageBinding { "type", (a, n) => { a.Type = n.GetScalarValue(); } }, { "headers", (a, n) => { a.Headers = n.GetScalarValue(); } }, { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, - { "expiry", (a, n) => { a.Expiry = n.GetIntegerValue(); } }, + { "expiry", (a, n) => { a.Expiry = n.GetLongValue(); } }, }; public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeV3(writer); diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs index cff2537..f396832 100644 --- a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs @@ -34,7 +34,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) writer.WriteOptionalProperty("groupId", this.GroupId); writer.WriteOptionalProperty("ccdtQueueManagerName", this.CcdtQueueManagerName); writer.WriteOptionalProperty("cipherSpec", this.CipherSpec); - writer.WriteOptionalProperty("multiEndpointServer", this.MultiEndpointServer); + writer.WriteOptionalProperty("multiEndpointServer", this.MultiEndpointServer); writer.WriteOptionalProperty("heartBeatInterval", this.HeartBeatInterval); writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); writer.WriteExtensions(this.Extensions); diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs index 9b2f5d4..9405eb8 100644 --- a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs @@ -13,8 +13,8 @@ public void Serialize(IAsyncApiWriter writer) { writer.WriteStartObject(); writer.WriteOptionalProperty("objectName", this.ObjectName); - writer.WriteOptionalProperty("isPartitioned", this.IsPartitioned); - writer.WriteOptionalProperty("exclusive", this.Exclusive); + writer.WriteOptionalProperty("isPartitioned", this.IsPartitioned); + writer.WriteOptionalProperty("exclusive", this.Exclusive); writer.WriteEndObject(); } } diff --git a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs index 2c6bb46..0720565 100644 --- a/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs @@ -15,8 +15,8 @@ public void Serialize(IAsyncApiWriter writer) writer.WriteStartObject(); writer.WriteOptionalProperty("string", this.StringValue); writer.WriteOptionalProperty("objectName", this.ObjectName); - writer.WriteOptionalProperty("durablePermitted", this.DurablePermitted); - writer.WriteOptionalProperty("lastMsgRetained", this.LastMsgRetained); + writer.WriteOptionalProperty("durablePermitted", this.DurablePermitted); + writer.WriteOptionalProperty("lastMsgRetained", this.LastMsgRetained); writer.WriteEndObject(); } } diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs index 12647c0..b3c6732 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs @@ -24,7 +24,7 @@ public class MQTT5ServerBinding : ServerBinding public override void SerializeV2(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); public override void SerializeV3(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); - public override void SerializeProperties(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi2_0); + public override void SerializeProperties(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_0); private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version) { diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/AnypointMQ/AnypointMQBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/AnypointMQ/AnypointMQBindings_Should.cs index 04f2759..50c9ad6 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/AnypointMQ/AnypointMQBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/AnypointMQ/AnypointMQBindings_Should.cs @@ -18,7 +18,8 @@ public void ChannelBinding_RoundTripsNestedPropertiesAndOmittedVersion(AsyncApiV var yaml = original.SerializeAsYaml(version); yaml.Should().Contain("destination: orders").And.Contain("destinationType: queue").And.NotContain("bindingVersion"); var settings = new AsyncApiReaderSettings { Bindings = new[] { new AnypointMQChannelBinding() } }; - var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out _); + var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out var diagnostics); + diagnostics.Errors.Should().BeEmpty(); parsed.Bindings["anypointmq"].Should().BeEquivalentTo(original.Bindings["anypointmq"]); } @@ -37,7 +38,8 @@ public void MessageBinding_RoundTripsHeadersSchema(AsyncApiVersion version) var yaml = original.SerializeAsYaml(version); yaml.Should().Contain("headers:").And.Contain("traceId:").And.Contain("bindingVersion: 0.1.0"); var settings = new AsyncApiReaderSettings { Bindings = new[] { new AnypointMQMessageBinding() } }; - var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out _); + var parsed = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out var diagnostics); + diagnostics.Errors.Should().BeEmpty(); parsed.Bindings["anypointmq"].Should().BeEquivalentTo(original.Bindings["anypointmq"]); } diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/GooglePubSub/GooglePubSubBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/GooglePubSub/GooglePubSubBindings_Should.cs index 2f26417..2a9801e 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/GooglePubSub/GooglePubSubBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/GooglePubSub/GooglePubSubBindings_Should.cs @@ -35,7 +35,8 @@ public void GooglePubSubBindings_RoundTripShape(AsyncApiVersion version) .And.Contain("allowedPersistenceRegions:") .And.Contain("firstRevisionId: '1'"); var settings = new AsyncApiReaderSettings { Bindings = Parsers }; - var parsedChannel = new AsyncApiStringReader(settings).ReadFragment(channelYaml, version, out _); + var parsedChannel = new AsyncApiStringReader(settings).ReadFragment(channelYaml, version, out var channelDiagnostics); + channelDiagnostics.Errors.Should().BeEmpty(); parsedChannel.Should().BeEquivalentTo(channel); var message = new AsyncApiMessage(); @@ -47,7 +48,8 @@ public void GooglePubSubBindings_RoundTripShape(AsyncApiVersion version) }); var messageYaml = message.SerializeAsYaml(version); messageYaml.Should().Contain("orderingKey: customer-7").And.Contain("projects/p/schemas/orders"); - var parsedMessage = new AsyncApiStringReader(settings).ReadFragment(messageYaml, version, out _); + var parsedMessage = new AsyncApiStringReader(settings).ReadFragment(messageYaml, version, out var messageDiagnostics); + messageDiagnostics.Errors.Should().BeEmpty(); parsedMessage.Should().BeEquivalentTo(message); } } diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/IBMMQ/IBMMQBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/IBMMQ/IBMMQBindings_Should.cs index 3e19212..3317a63 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/IBMMQ/IBMMQBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/IBMMQ/IBMMQBindings_Should.cs @@ -36,10 +36,10 @@ public void IBMMQBindings_RoundTripAndOmitUnsetValues(AsyncApiVersion version) channel.Bindings.Add(new IBMMQChannelBinding { DestinationType = "queue", MaxMsgLength = 0, - Queue = new Queue { ObjectName = "ORDERS", IsPartitioned = false, Exclusive = true }, + Queue = new Queue { ObjectName = "ORDERS", IsPartitioned = false, Exclusive = false }, }); var channelYaml = channel.SerializeAsYaml(version); - channelYaml.Should().Contain("isPartitioned: false").And.Contain("maxMsgLength: 0").And.NotContain("topic:"); + channelYaml.Should().Contain("isPartitioned: false").And.Contain("exclusive: false").And.Contain("maxMsgLength: 0").And.NotContain("topic:"); new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) .ReadFragment(channelYaml, version, out _).Should().BeEquivalentTo(channel); @@ -47,24 +47,45 @@ public void IBMMQBindings_RoundTripAndOmitUnsetValues(AsyncApiVersion version) topicChannel.Bindings.Add(new IBMMQChannelBinding { DestinationType = "topic", - Topic = new Topic { StringValue = "orders/#", ObjectName = "ORDERS.TOPIC", DurablePermitted = false, LastMsgRetained = true }, + Topic = new Topic { StringValue = "orders/#", ObjectName = "ORDERS.TOPIC", DurablePermitted = false, LastMsgRetained = false }, }); var topicYaml = topicChannel.SerializeAsYaml(version); - topicYaml.Should().Contain("string: orders/#").And.NotContain("queue:"); + topicYaml.Should().Contain("string: orders/#").And.Contain("durablePermitted: false").And.Contain("lastMsgRetained: false").And.NotContain("queue:"); new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) .ReadFragment(topicYaml, version, out _).Should().BeEquivalentTo(topicChannel); var message = new AsyncApiMessage(); - message.Bindings.Add(new IBMMQMessageBinding { Type = "binary", Headers = "STRING", Description = "event headers", Expiry = 0 }); + message.Bindings.Add(new IBMMQMessageBinding { Type = "binary", Headers = "STRING", Description = "event headers", Expiry = 2592000000L }); var messageYaml = message.SerializeAsYaml(version); - messageYaml.Should().Contain("headers: STRING").And.Contain("expiry: 0"); + messageYaml.Should().Contain("headers: STRING").And.Contain("expiry: 2592000000"); new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) .ReadFragment(messageYaml, version, out _).Should().BeEquivalentTo(message); + var zeroExpiryMessage = new AsyncApiMessage(); + zeroExpiryMessage.Bindings.Add(new IBMMQMessageBinding { Expiry = 0 }); + var zeroExpiryYaml = zeroExpiryMessage.SerializeAsYaml(version); + zeroExpiryYaml.Should().Contain("expiry: 0"); + new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) + .ReadFragment(zeroExpiryYaml, version, out _).Should().BeEquivalentTo(zeroExpiryMessage); + var absent = new AsyncApiServer(); absent.Bindings.Add(new IBMMQServerBinding()); var absentYaml = absent.SerializeAsYaml(version); absentYaml.Should().NotContain("multiEndpointServer").And.NotContain("heartBeatInterval").And.NotContain("bindingVersion"); + + var absentChannel = new AsyncApiChannel(); + absentChannel.Bindings.Add(new IBMMQChannelBinding { Queue = new Queue() }); + var absentChannelYaml = absentChannel.SerializeAsYaml(version); + absentChannelYaml.Should().NotContain("isPartitioned").And.NotContain("exclusive"); + new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) + .ReadFragment(absentChannelYaml, version, out _).Should().BeEquivalentTo(absentChannel); + + var absentTopicChannel = new AsyncApiChannel(); + absentTopicChannel.Bindings.Add(new IBMMQChannelBinding { Topic = new Topic() }); + var absentTopicYaml = absentTopicChannel.SerializeAsYaml(version); + absentTopicYaml.Should().NotContain("durablePermitted").And.NotContain("lastMsgRetained"); + new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = Parsers }) + .ReadFragment(absentTopicYaml, version, out _).Should().BeEquivalentTo(absentTopicChannel); } } } diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/ROS2/ROS2Bindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/ROS2/ROS2Bindings_Should.cs index 4601ed0..1646dd2 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/ROS2/ROS2Bindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/ROS2/ROS2Bindings_Should.cs @@ -74,5 +74,41 @@ public void ServerBinding_OmitsUnsetFieldsAndWritesExplicitZero(AsyncApiVersion diagnostics.Warnings.Should().BeEmpty(); parsed.Bindings["ros2"].Should().BeEquivalentTo(binding); } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBinding_RoundTripsUpperBoundaryDomainId(AsyncApiVersion version) + { + var binding = new ROS2ServerBinding { DomainId = 231 }; + var server = new AsyncApiServer { Host = "localhost", Protocol = "ros2" }; + server.Bindings.Add(binding); + + var actual = server.SerializeAsYaml(version); + actual.Should().Contain("domainId: 231"); + + var settings = new AsyncApiReaderSettings { Bindings = new[] { new ROS2ServerBinding() } }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(actual, version, out var diagnostics); + diagnostics.Errors.Should().BeEmpty(); + diagnostics.Warnings.Should().BeEmpty(); + parsed.Bindings["ros2"].Should().BeEquivalentTo(binding); + } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void ServerBinding_OmitsUnsetDomainId(AsyncApiVersion version) + { + var binding = new ROS2ServerBinding(); + var server = new AsyncApiServer { Host = "localhost", Protocol = "ros2" }; + server.Bindings.Add(binding); + + var actual = server.SerializeAsYaml(version); + actual.Should().NotContain("domainId:"); + + var settings = new AsyncApiReaderSettings { Bindings = new[] { new ROS2ServerBinding() } }; + var parsed = new AsyncApiStringReader(settings).ReadFragment(actual, version, out var diagnostics); + diagnostics.Errors.Should().BeEmpty(); + diagnostics.Warnings.Should().BeEmpty(); + parsed.Bindings["ros2"].Should().BeEquivalentTo(binding); + } } }