From 124564838da5fe5407d4740e7d78c3b3f0821c39 Mon Sep 17 00:00:00 2001 From: VisualBean Date: Mon, 25 Mar 2024 12:56:43 +0100 Subject: [PATCH] feat: add amqp bindings --- .../AMQP/AMQPChannelBinding.cs | 86 +++++++++ .../AMQP/AMQPMessageBinding.cs | 51 +++++ .../AMQP/AMQPOperationBinding.cs | 103 ++++++++++ .../AMQP/ChannelType.cs | 15 ++ .../AMQP/DeliveryMode.cs | 15 ++ src/LEGO.AsyncAPI.Bindings/AMQP/Exchange.cs | 50 +++++ .../AMQP/ExchangeType.cs | 24 +++ src/LEGO.AsyncAPI.Bindings/AMQP/Queue.cs | 50 +++++ .../BindingsCollection.cs | 9 + .../Kafka/TopicConfigurationObject.cs | 8 +- .../ParseNodes/ParseNode.cs | 8 +- .../ParseNodes/ValueNode.cs | 8 +- .../Bindings/AMQP/AMQPBindings_Should.cs | 181 ++++++++++++++++++ 13 files changed, 596 insertions(+), 12 deletions(-) create mode 100644 src/LEGO.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs create mode 100644 src/LEGO.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs create mode 100644 src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs create mode 100644 src/LEGO.AsyncAPI.Bindings/AMQP/ChannelType.cs create mode 100644 src/LEGO.AsyncAPI.Bindings/AMQP/DeliveryMode.cs create mode 100644 src/LEGO.AsyncAPI.Bindings/AMQP/Exchange.cs create mode 100644 src/LEGO.AsyncAPI.Bindings/AMQP/ExchangeType.cs create mode 100644 src/LEGO.AsyncAPI.Bindings/AMQP/Queue.cs create mode 100644 test/LEGO.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs new file mode 100644 index 00000000..f77dc37f --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs @@ -0,0 +1,86 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for AMQP channel settings. + /// + public class AMQPChannelBinding : ChannelBinding + { + /// + /// Defines what type of channel is it. Can be either queue or routingKey. + /// + public ChannelType Is { get; set; } + + /// + /// When is=routingKey, this object defines the exchange properties. + /// + public Exchange Exchange { get; set; } + + /// + /// When is=queue, this object defines the queue properties. + /// + public Queue Queue { get; set; } + + public override string BindingKey => "amqp"; + + protected override FixedFieldMap FixedFieldMap => new () + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "is", (a, n) => { a.Is = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "exchange", (a, n) => { a.Exchange = n.ParseMap(ExchangeFixedFields); } }, + { "queue", (a, n) => { a.Queue = n.ParseMap(QueueFixedFields); } }, + }; + + private static FixedFieldMap ExchangeFixedFields = new () + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "durable", (a, n) => { a.Durable = n.GetBooleanValue(); } }, + { "type", (a, n) => { a.Type = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "autoDelete", (a, n) => { a.AutoDelete = n.GetBooleanValue(); } }, + { "vhost", (a, n) => { a.Vhost = n.GetScalarValue(); } }, + }; + + private static FixedFieldMap QueueFixedFields = new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "durable", (a, n) => { a.Durable = n.GetBooleanValue(); } }, + { "exclusive", (a, n) => { a.Exclusive = n.GetBooleanValue(); } }, + { "autoDelete", (a, n) => { a.AutoDelete = n.GetBooleanValue(); } }, + { "vhost", (a, n) => { a.Vhost = n.GetScalarValue(); } }, + }; + + /// + /// Serialize to AsyncAPI V2 document without using reference. + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("is", this.Is.GetDisplayName()); + switch (this.Is) + { + case ChannelType.RoutingKey: + writer.WriteOptionalObject("exchange", this.Exchange, (w, t) => t.Serialize(w)); + break; + case ChannelType.Queue: + writer.WriteOptionalObject("queue", this.Queue, (w, t) => t.Serialize(w)); + break; + } + + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs new file mode 100644 index 00000000..17642020 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs @@ -0,0 +1,51 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for AMQP messages. + /// + public class AMQPMessageBinding : MessageBinding + { + /// + /// A MIME encoding for the message content. + /// + public string ContentEncoding { get; set; } + + /// + /// Application-specific message type. + /// + public string MessageType { get; set; } + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + + writer.WriteOptionalProperty("contentEncoding", this.ContentEncoding); + writer.WriteOptionalProperty("messageType", this.MessageType); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + + public override string BindingKey => "amqp"; + + protected override FixedFieldMap FixedFieldMap => new () + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "contentEncoding", (a, n) => { a.ContentEncoding = n.GetScalarValue(); } }, + { "messageType", (a, n) => { a.MessageType = n.GetScalarValue(); } }, + }; + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs new file mode 100644 index 00000000..86dc74ef --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs @@ -0,0 +1,103 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for AMQP operations. + /// + public class AMQPOperationBinding : OperationBinding + { + /// + /// TTL (Time-To-Live) for the message. It MUST be greater than or equal to zero. + /// + public uint? Expiration { get; set; } + + /// + /// Identifies the user who has sent the message. + /// + public string UserId { get; set; } + + /// + /// The routing keys the message should be routed to at the time of publishing. + /// + public List Cc { get; set; } = new List(); + + /// + /// A priority for the message. + /// + public int? Priority { get; set; } + + /// + /// Delivery mode of the message. Its value MUST be either 1 (transient) or 2 (persistent). + /// + public DeliveryMode? DeliveryMode { get; set; } + + /// + /// Whether the message is mandatory or not. + /// + public bool? Mandatory { get; set; } + + /// + /// Like cc but consumers will not receive this information. + /// + public List Bcc { get; set; } = new List(); + + /// + /// Whether the message should include a timestamp or not. + /// + public bool? Timestamp { get; set; } + + /// + /// Whether the consumer should ack the message or not. + /// + public bool? Ack { get; set; } + + public override string BindingKey => "amqp"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "expiration", (a, n) => { a.Expiration = (uint?)n.GetIntegerValueOrDefault(); } }, + { "userId", (a, n) => { a.UserId = n.GetScalarValueOrDefault(); } }, + { "cc", (a, n) => { a.Cc = n.CreateSimpleList(s => s.GetScalarValue()); } }, + { "priority", (a, n) => { a.Priority = n.GetIntegerValueOrDefault(); } }, + { "deliveryMode", (a, n) => { a.DeliveryMode = (DeliveryMode?)n.GetIntegerValueOrDefault(); } }, + { "mandatory", (a, n) => { a.Mandatory = n.GetBooleanValueOrDefault(); } }, + { "bcc", (a, n) => { a.Bcc = n.CreateSimpleList(s => s.GetScalarValue()); } }, + { "timestamp", (a, n) => { a.Timestamp = n.GetBooleanValueOrDefault(); } }, + { "ack", (a, n) => { a.Ack = n.GetBooleanValueOrDefault(); } }, + }; + + /// + /// Serialize to AsyncAPI V2 document without using reference. + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalProperty("expiration", (int)this.Expiration); + writer.WriteOptionalProperty("userId", this.UserId); + writer.WriteOptionalCollection("cc", this.Cc, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty("priority", this.Priority); + writer.WriteOptionalProperty("deliveryMode", (int?)this.DeliveryMode); + writer.WriteOptionalProperty("mandatory", this.Mandatory); + writer.WriteOptionalCollection("bcc", this.Bcc, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty("timestamp", this.Timestamp); + writer.WriteOptionalProperty("ack", this.Ack); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/ChannelType.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/ChannelType.cs new file mode 100644 index 00000000..a817ea49 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/ChannelType.cs @@ -0,0 +1,15 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Attributes; + + public enum ChannelType + { + [Display("routingKey")] + RoutingKey = 0, + + [Display("queue")] + Queue, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/DeliveryMode.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/DeliveryMode.cs new file mode 100644 index 00000000..77b855b5 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/DeliveryMode.cs @@ -0,0 +1,15 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Attributes; + + public enum DeliveryMode + { + [Display("transient")] + Transient = 1, + + [Display("persistent")] + Persistent = 2, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/Exchange.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/Exchange.cs new file mode 100644 index 00000000..66815b3c --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/Exchange.cs @@ -0,0 +1,50 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + /// + /// Represents an exchange configuration. + /// + public class Exchange : IAsyncApiElement + { + /// + /// The name of the exchange. It MUST NOT exceed 255 characters long. + /// + public string Name { get; set; } + + /// + /// The type of the exchange. Can be either topic, direct, fanout, default, or headers. + /// + public ExchangeType Type { get; set; } + + /// + /// Whether the exchange should survive broker restarts or not. + /// + public bool Durable { get; set; } + + /// + /// Whether the exchange should be deleted when the last queue is unbound from it. + /// + public bool AutoDelete { get; set; } + + /// + /// The virtual host of the exchange. Defaults to /. + /// + public string Vhost { get; set; } = "/"; + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteRequiredProperty(AsyncApiConstants.Name, this.Name); + writer.WriteRequiredProperty(AsyncApiConstants.Type, this.Type.GetDisplayName()); + writer.WriteRequiredProperty("durable", this.Durable); + writer.WriteRequiredProperty("autoDelete", this.AutoDelete); + writer.WriteRequiredProperty("vhost", this.Vhost); + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/ExchangeType.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/ExchangeType.cs new file mode 100644 index 00000000..1cfde013 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/ExchangeType.cs @@ -0,0 +1,24 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Attributes; + + public enum ExchangeType + { + [Display("default")] + Default = 0, + + [Display("topic")] + Topic, + + [Display("direct")] + Direct, + + [Display("fanout")] + Fanout, + + [Display("headers")] + Headers, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/Queue.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/Queue.cs new file mode 100644 index 00000000..a3cf25d3 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/Queue.cs @@ -0,0 +1,50 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + /// + /// Represents a queue configuration. + /// + public class Queue : IAsyncApiElement + { + /// + /// The name of the queue. It MUST NOT exceed 255 characters long. + /// + public string Name { get; set; } + + /// + /// Whether the queue should survive broker restarts or not. + /// + public bool Durable { get; set; } + + /// + /// Whether the queue should be used only by one connection or not. + /// + public bool Exclusive { get; set; } + + /// + /// Whether the queue should be deleted when the last consumer unsubscribes. + /// + public bool AutoDelete { get; set; } + + /// + /// The virtual host of the queue. Defaults to /. + /// + public string Vhost { get; set; } = "/"; + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteRequiredProperty(AsyncApiConstants.Name, this.Name); + writer.WriteRequiredProperty("durable", this.Durable); + writer.WriteRequiredProperty("exclusive", this.Exclusive); + writer.WriteRequiredProperty("autoDelete", this.AutoDelete); + writer.WriteRequiredProperty("vhost", this.Vhost); + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs index ccfad8a4..0fb7876f 100644 --- a/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs +++ b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs @@ -3,6 +3,7 @@ namespace LEGO.AsyncAPI.Bindings { using System; using System.Collections.Generic; + using LEGO.AsyncAPI.Bindings.AMQP; using LEGO.AsyncAPI.Bindings.Http; using LEGO.AsyncAPI.Bindings.Kafka; using LEGO.AsyncAPI.Bindings.Pulsar; @@ -51,6 +52,7 @@ public static TCollection Add( Websockets, Sqs, Sns, + AMQP, }; public static IEnumerable> Http => new List> @@ -89,5 +91,12 @@ public static TCollection Add( new SnsChannelBinding(), new SnsOperationBinding(), }; + + public static IEnumerable> AMQP => new List> + { + new AMQPChannelBinding(), + new AMQPOperationBinding(), + new AMQPMessageBinding(), + }; } } diff --git a/src/LEGO.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs index da0027c8..a3fd1e7a 100644 --- a/src/LEGO.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs @@ -33,22 +33,22 @@ public class TopicConfigurationObject : IAsyncApiElement /// The max.message.bytes configuration option. /// public int? MaxMessageBytes { get; set; } - + /// /// The confluent.key.schema.validation configuration option. /// public bool? ConfluentKeySchemaValidation { get; set; } - + /// /// The confluent.key.subject.name.strategy configuration option. /// public string ConfluentKeySubjectName { get; set; } - + /// /// The confluent.value.schema.validation configuration option. /// public bool? ConfluentValueSchemaValidation { get; set; } - + /// /// The confluent.value.subject.name.strategy configuration option. /// diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/ParseNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/ParseNode.cs index 838eb358..da3e8a1a 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/ParseNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/ParseNode.cs @@ -94,7 +94,7 @@ public virtual string GetScalarValue() throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } - public virtual string GetScalarValueOrDefault(string defaultValue) + public virtual string GetScalarValueOrDefault(string defaultValue = null) { throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } @@ -104,7 +104,7 @@ public virtual bool GetBooleanValue() throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } - public virtual bool? GetBooleanValueOrDefault(bool? defaultValue) + public virtual bool? GetBooleanValueOrDefault(bool? defaultValue = null) { throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } @@ -114,7 +114,7 @@ public virtual int GetIntegerValue() throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } - public virtual int? GetIntegerValueOrDefault(int? defaultValue) + public virtual int? GetIntegerValueOrDefault(int? defaultValue = null) { throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } @@ -124,7 +124,7 @@ public virtual long GetLongValue() throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } - public virtual long? GetLongValueOrDefault(long? defaultValue) + public virtual long? GetLongValueOrDefault(long? defaultValue = null) { throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs index 17ec9ac7..e580afe6 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs @@ -32,7 +32,7 @@ public override string GetScalarValue() return this.cachedScalarValue; } - public override string GetScalarValueOrDefault(string defaultValue) + public override string GetScalarValueOrDefault(string defaultValue = null) { var value = this.GetScalarValue(); if (value is not null) @@ -53,7 +53,7 @@ public override int GetIntegerValue() throw new AsyncApiReaderException("Value could not parse to integer."); } - public override int? GetIntegerValueOrDefault(int? defaultValue) + public override int? GetIntegerValueOrDefault(int? defaultValue = null) { if (int.TryParse(this.GetScalarValue(), out int value)) { @@ -73,7 +73,7 @@ public override long GetLongValue() throw new AsyncApiReaderException("Value could not parse to long."); } - public override long? GetLongValueOrDefault(long? defaultValue) + public override long? GetLongValueOrDefault(long? defaultValue = null) { if (long.TryParse(this.GetScalarValue(), out long value)) { @@ -93,7 +93,7 @@ public override bool GetBooleanValue() throw new AsyncApiReaderException("Value could not parse to bool."); } - public override bool? GetBooleanValueOrDefault(bool? defaultValue) + public override bool? GetBooleanValueOrDefault(bool? defaultValue = null) { if (bool.TryParse(this.GetScalarValue(), out bool value)) { diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs new file mode 100644 index 00000000..09317d0c --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs @@ -0,0 +1,181 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings.AMQP +{ + using System.Collections.Generic; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.AMQP; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + + public class AMQPBindings_Should + { + [Test] + public void AMQPChannelBinding_WithRoutingKey_SerializesAndDeserializes() + { + // Arrange + var expected = +@"bindings: + amqp: + is: routingKey + exchange: + name: myExchange + type: topic + durable: true + autoDelete: false + vhost: /"; + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new AMQPChannelBinding + { + Is = ChannelType.RoutingKey, + Exchange = new Exchange + { + Name = "myExchange", + Type = ExchangeType.Topic, + Durable = true, + AutoDelete = false, + Vhost = "/", + }, + }); + + // Act + var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.AMQP; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + Assert.AreEqual(expected, actual); + binding.Should().BeEquivalentTo(channel); + } + + [Test] + public void AMQPChannelBinding_WithQueue_SerializesAndDeserializes() + { + // Arrange + var expected = +@"bindings: + amqp: + is: queue + queue: + name: my-queue-name + durable: true + exclusive: true + autoDelete: false + vhost: /"; + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new AMQPChannelBinding + { + Is = ChannelType.Queue, + Queue = new Queue + { + Name = "my-queue-name", + Durable = true, + Exclusive = true, + AutoDelete = false, + Vhost = "/", + }, + }); + + // Act + var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.AMQP; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + Assert.AreEqual(expected, actual); + binding.Should().BeEquivalentTo(channel); + } + + [Test] + public void AMQPMessageBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = +@"bindings: + amqp: + contentEncoding: gzip + messageType: user.signup"; + + var message = new AsyncApiMessage(); + + message.Bindings.Add(new AMQPMessageBinding + { + ContentEncoding = "gzip", + MessageType = "user.signup", + }); + + // Act + var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.AMQP; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + Assert.AreEqual(expected, actual); + binding.Should().BeEquivalentTo(message); + } + + [Test] + public void AMQPOperationBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = +@"bindings: + amqp: + expiration: 100000 + userId: guest + cc: + - user.logs + priority: 10 + deliveryMode: 2 + mandatory: false + bcc: + - external.audit + timestamp: true + ack: false"; + + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new AMQPOperationBinding + { + Expiration = 100000, + UserId = "guest", + Cc = new List { "user.logs" }, + Priority = 10, + DeliveryMode = DeliveryMode.Persistent, + Mandatory = false, + Bcc = new List { "external.audit" }, + Timestamp = true, + Ack = false, + });; + + // Act + var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.AMQP; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + Assert.AreEqual(expected, actual); + binding.Should().BeEquivalentTo(operation); + } + } +}