diff --git a/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs new file mode 100644 index 0000000..7c76aa0 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQChannelBinding.cs @@ -0,0 +1,52 @@ +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(); } }, + }; + + /// + /// 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)); + 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..82cf7eb --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/AnypointMQ/AnypointMQMessageBinding.cs @@ -0,0 +1,58 @@ +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(); } }, + }; + + /// + /// 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)); + writer.WriteStartObject(); + writer.WriteOptionalObject("headers", this.Headers, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + 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)); + 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..acff5b1 100644 --- a/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs +++ b/src/ByteBard.AsyncAPI.Bindings/BindingHelpers.cs @@ -1,11 +1,21 @@ namespace ByteBard.AsyncAPI.Bindings { + using System; + using System.Collections.Generic; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Readers; using ByteBard.AsyncAPI.Readers.ParseNodes; 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() { @@ -16,10 +26,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 +55,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..4c5ff39 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,37 @@ public static class BindingsCollection { - public static TCollection Add( - this TCollection destination, - IEnumerable source) - where TCollection : ICollection + /// + /// 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 { - 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 +63,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..d24a8ea --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubChannelBinding.cs @@ -0,0 +1,68 @@ +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(); } }, + }; + + /// + /// 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)); + 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..b934ace --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/GooglePubSubMessageBinding.cs @@ -0,0 +1,57 @@ +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(); } }, + }; + + /// + /// 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)); + 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..a31a0bd --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageSchema.cs @@ -0,0 +1,21 @@ +namespace ByteBard.AsyncAPI.Bindings.GooglePubSub +{ + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + 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(); + 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..c5f8d42 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/MessageStoragePolicy.cs @@ -0,0 +1,22 @@ +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; } + + /// + /// Writes the message storage policy object, omitting null or empty persistence regions. + /// + /// is null. + 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..9b6a955 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/GooglePubSub/SchemaSettings.cs @@ -0,0 +1,27 @@ +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; } + + /// + /// Writes the schema settings object, omitting unset values. + /// + /// is null. + 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..8608aee --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQChannelBinding.cs @@ -0,0 +1,69 @@ +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(); } }, + }; + + /// + /// 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)); + 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..d6e7bb7 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQMessageBinding.cs @@ -0,0 +1,54 @@ +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 long? 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.GetLongValue(); } }, + }; + + /// + /// 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)); + 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..9cfd800 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/IBMMQServerBinding.cs @@ -0,0 +1,57 @@ +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(); } }, + }; + + /// + /// 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)); + 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..583d5cc --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Queue.cs @@ -0,0 +1,25 @@ +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; } + + /// + /// Writes the IBM MQ queue settings object, omitting unset values. + /// + /// is null. + 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..2bac7ef --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/IBMMQ/Topic.cs @@ -0,0 +1,27 @@ +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; } + + /// + /// Writes the IBM MQ topic settings object, omitting unset values. + /// + /// is null. + 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..2a53bca --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSChannelBinding.cs @@ -0,0 +1,52 @@ +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(); } }, + }; + + /// + /// 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)); + 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..fd307ec --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSMessageBinding.cs @@ -0,0 +1,58 @@ +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(); } }, + }; + + /// + /// 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)); + writer.WriteStartObject(); + writer.WriteOptionalObject("headers", this.Headers, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + 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)); + 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..5709695 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSProperty.cs @@ -0,0 +1,26 @@ +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; } + + /// + /// 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(); + 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..a9efc18 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/JMS/JMSServerBinding.cs @@ -0,0 +1,63 @@ +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"; + + /// + /// 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)); + 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..7fed53b 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,33 @@ 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()); + 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..6b8a196 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,17 @@ 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(); + + /// + /// 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) @@ -70,7 +94,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..93214f6 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -31,19 +31,50 @@ 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; } + + /// + /// Writes the MQTT message binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) { - this.SerializeV3(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); + 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) { @@ -52,9 +83,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 +96,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..193f4e6 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,85 @@ /// 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); } }, }; + /// + /// Writes the MQTT operation binding object and its extensions for AsyncAPI 2.0. + /// + /// is null. public override void SerializeV2(IAsyncApiWriter writer) { - this.SerializeV3(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); + 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) { @@ -52,9 +96,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..0876fa9 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() @@ -66,17 +80,41 @@ public class MQTTServerBinding : ServerBinding { "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.SerializeV3(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); + 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) { @@ -84,15 +122,33 @@ 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(); } + + /// + /// 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); + 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..71f809b --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs @@ -0,0 +1,145 @@ +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 + { + /// + /// Stores the supplied integer and schema without validating that exactly one is present. + /// + 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; } + + /// + /// 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 + ? FromLong(node.GetLongValue()) + : 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) + { + 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 + { + /// + /// Stores the supplied response topic and schema without validating that exactly one is present. + /// + private MQTTStringOrSchema(string value, AsyncApiJsonSchema schema) + { + this.Value = value; + this.Schema = schema; + } + + public string Value { get; } + + 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 + ? FromString(node.GetScalarValue()) + : 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) + { + 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..8d2688b --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT5/MQTT5ServerBinding.cs @@ -0,0 +1,57 @@ +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); } }, + }; + + /// + /// 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); + + public override void SerializeProperties(IAsyncApiWriter writer) => this.SerializeProperties(writer, AsyncApiVersion.AsyncApi3_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)); + 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..0f59ff0 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/NATS/NATSOperationBinding.cs @@ -0,0 +1,47 @@ +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() }, + }; + + /// + /// 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)); + 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..8cddc9c --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2OperationBinding.cs @@ -0,0 +1,50 @@ +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); } }, + }; + + /// + /// 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)); + 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..bd33c46 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs @@ -0,0 +1,47 @@ +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(); } }, + }; + + /// + /// Writes the QoS policy object, omitting unset values and preserving zero and negative numeric values. + /// + /// is null. + 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..9512645 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs @@ -0,0 +1,51 @@ +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(); } }, + }; + + /// + /// 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)); + 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..896c00f --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceDestination.cs @@ -0,0 +1,42 @@ +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; } + + /// + /// 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)); + 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..ee4b18d --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs @@ -0,0 +1,83 @@ +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; + } + } + + /// + /// 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) + { + throw new InvalidOperationException("A Solace integer-or-schema value must contain an integer or a schema."); + } + } + + /// + /// 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)); + 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..4b3dc48 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs @@ -0,0 +1,93 @@ +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() }, + }; + + /// + /// 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) + { + return new SolaceIntegerOrSchema { Integer = node.GetLongValue() }; + } + + 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)); + 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..ae1d61c --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceQueue.cs @@ -0,0 +1,43 @@ +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() }, + }; + + /// + /// 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)); + 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..d7756d1 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceServerBinding.cs @@ -0,0 +1,48 @@ +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() }, + }; + + /// + /// 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)); + 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..f923b81 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Bindings/Solace/SolaceTopic.cs @@ -0,0 +1,31 @@ +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()) }, + }; + + /// + /// 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)); + 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..50c9ad6 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/AnypointMQ/AnypointMQBindings_Should.cs @@ -0,0 +1,47 @@ +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 var diagnostics); + diagnostics.Errors.Should().BeEmpty(); + + 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 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 new file mode 100644 index 0000000..2a9801e --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/GooglePubSub/GooglePubSubBindings_Should.cs @@ -0,0 +1,56 @@ +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 var channelDiagnostics); + channelDiagnostics.Errors.Should().BeEmpty(); + 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 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 new file mode 100644 index 0000000..3317a63 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/IBMMQ/IBMMQBindings_Should.cs @@ -0,0 +1,91 @@ +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 = false }, + }); + var channelYaml = channel.SerializeAsYaml(version); + 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); + + var topicChannel = new AsyncApiChannel(); + topicChannel.Bindings.Add(new IBMMQChannelBinding + { + DestinationType = "topic", + Topic = new Topic { StringValue = "orders/#", ObjectName = "ORDERS.TOPIC", DurablePermitted = false, LastMsgRetained = false }, + }); + var topicYaml = topicChannel.SerializeAsYaml(version); + 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 = 2592000000L }); + var messageYaml = message.SerializeAsYaml(version); + 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/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 53fb4b3..7b5eac9 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..1646dd2 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/ROS2/ROS2Bindings_Should.cs @@ -0,0 +1,114 @@ +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); + } + + [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); + } + } +} 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); + } + } +}