Refactor the API interfaces (#21)

* Refactor the API 
* Use the interfaces to define the targets for queues, exchanges and bindings 
* Implement the producer message target-based
---------

Signed-off-by: Gabriele Santomaggio <G.santomaggio@gmail.com>
This commit is contained in:
Gabriele Santomaggio 2025-01-30 11:29:44 +01:00 committed by GitHub
parent bfc8b02de9
commit 89c4dd74a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 909 additions and 281 deletions

View File

@ -10,4 +10,10 @@ This library is in early stages of development. It is meant to be used with Rabb
## Getting Started
You can find an example in: `examples/getting_started`
You can find an example in: `docs/examples/getting_started`
## Examples
You can find more examples in: `docs/examples`

View File

@ -37,7 +37,7 @@ func main() {
// Create the management interface for the connection
// so we can declare exchanges, queues, and bindings
management := amqpConnection.Management()
exchangeInfo, err := management.DeclareExchange(context.TODO(), &rabbitmq_amqp.ExchangeSpecification{
exchangeInfo, err := management.DeclareExchange(context.TODO(), &rabbitmq_amqp.TopicExchangeSpecification{
Name: exchangeName,
})
if err != nil {
@ -46,9 +46,8 @@ func main() {
}
// Declare a Quorum queue
queueInfo, err := management.DeclareQueue(context.TODO(), &rabbitmq_amqp.QueueSpecification{
Name: queueName,
QueueType: rabbitmq_amqp.QueueType{Type: rabbitmq_amqp.Quorum},
queueInfo, err := management.DeclareQueue(context.TODO(), &rabbitmq_amqp.QuorumQueueSpecification{
Name: queueName,
})
if err != nil {
@ -57,7 +56,7 @@ func main() {
}
// Bind the queue to the exchange
bindingPath, err := management.Bind(context.TODO(), &rabbitmq_amqp.BindingSpecification{
bindingPath, err := management.Bind(context.TODO(), &rabbitmq_amqp.ExchangeToQueueBindingSpecification{
SourceExchange: exchangeName,
DestinationQueue: queueName,
BindingKey: routingKey,
@ -70,8 +69,10 @@ func main() {
// Create a consumer to receive messages from the queue
// you need to build the address of the queue, but you can use the helper function
addrQueue, _ := rabbitmq_amqp.QueueAddress(&queueName)
consumer, err := amqpConnection.Consumer(context.Background(), addrQueue, "getting-started-consumer")
consumer, err := amqpConnection.NewConsumer(context.Background(), &rabbitmq_amqp.QueueAddress{
Queue: queueName,
}, "getting-started-consumer")
if err != nil {
rabbitmq_amqp.Error("Error creating consumer", err)
return
@ -105,8 +106,10 @@ func main() {
}
}(consumerContext)
addr, _ := rabbitmq_amqp.ExchangeAddress(&exchangeName, &routingKey)
publisher, err := amqpConnection.Publisher(context.Background(), addr, "getting-started-publisher")
publisher, err := amqpConnection.NewPublisher(context.Background(), &rabbitmq_amqp.ExchangeAddress{
Exchange: exchangeName,
Key: routingKey,
}, "getting-started-publisher")
if err != nil {
rabbitmq_amqp.Error("Error creating publisher", err)
return
@ -151,10 +154,12 @@ func main() {
err = consumer.Close(context.Background())
if err != nil {
rabbitmq_amqp.Error("[Consumer]", err)
return
}
// Close the publisher
err = publisher.Close(context.Background())
if err != nil {
rabbitmq_amqp.Error("[Publisher]", err)
return
}

8
go.mod
View File

@ -5,18 +5,18 @@ go 1.22.0
require (
github.com/Azure/go-amqp v1.4.0-beta.1
github.com/google/uuid v1.6.0
github.com/onsi/ginkgo/v2 v2.20.2
github.com/onsi/gomega v1.34.2
github.com/onsi/ginkgo/v2 v2.22.1
github.com/onsi/gomega v1.36.2
)
require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.25.0 // indirect
golang.org/x/tools v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

20
go.sum
View File

@ -10,14 +10,14 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM=
github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
@ -28,10 +28,10 @@ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -3,12 +3,71 @@ package rabbitmq_amqp
import (
"errors"
"fmt"
"github.com/Azure/go-amqp"
"strings"
)
// Address Creates the address for the exchange or queue following the RabbitMQ conventions.
// TargetAddress is an interface that represents an address that can be used to send messages to.
// It can be either a Queue or an Exchange with a routing key.
type TargetAddress interface {
toAddress() (string, error)
}
// QueueAddress represents the address of a queue.
type QueueAddress struct {
Queue string // The name of the queue
Parameters string // Additional parameters not related to the queue. Most of the time it is empty
}
func (qas *QueueAddress) toAddress() (string, error) {
q := &qas.Queue
if isStringNilOrEmpty(&qas.Queue) {
q = nil
}
return queueAddress(q)
}
// ExchangeAddress represents the address of an exchange with a routing key.
type ExchangeAddress struct {
Exchange string // The name of the exchange
Key string // The routing key. Can be empty
}
func (eas *ExchangeAddress) toAddress() (string, error) {
ex := &eas.Exchange
if isStringNilOrEmpty(&eas.Exchange) {
ex = nil
}
k := &eas.Key
if isStringNilOrEmpty(&eas.Key) {
k = nil
}
return exchangeAddress(ex, k)
}
// MessageToAddressHelper sets the To property of the message to the address of the target.
// The target must be a QueueAddress or an ExchangeAddress.
// Note: The field To will be overwritten if it is already set.
func MessageToAddressHelper(msgRef *amqp.Message, target TargetAddress) error {
if target == nil {
return errors.New("target cannot be nil")
}
address, err := target.toAddress()
if err != nil {
return err
}
if msgRef.Properties == nil {
msgRef.Properties = &amqp.MessageProperties{}
}
msgRef.Properties.To = &address
return nil
}
// address Creates the address for the exchange or queue following the RabbitMQ conventions.
// see: https://www.rabbitmq.com/docs/next/amqp#address-v2
func Address(exchange, key, queue *string, urlParameters *string) (string, error) {
func address(exchange, key, queue *string, urlParameters *string) (string, error) {
if exchange == nil && queue == nil {
return "", errors.New("exchange or queue must be set")
}
@ -39,23 +98,23 @@ func Address(exchange, key, queue *string, urlParameters *string) (string, error
return "/" + queues + "/" + encodePathSegments(*queue) + urlAppend, nil
}
// ExchangeAddress Creates the address for the exchange
// See Address for more information
func ExchangeAddress(exchange, key *string) (string, error) {
return Address(exchange, key, nil, nil)
// exchangeAddress Creates the address for the exchange
// See address for more information
func exchangeAddress(exchange, key *string) (string, error) {
return address(exchange, key, nil, nil)
}
// QueueAddress Creates the address for the queue.
// See Address for more information
func QueueAddress(queue *string) (string, error) {
return Address(nil, nil, queue, nil)
// queueAddress Creates the address for the queue.
// See address for more information
func queueAddress(queue *string) (string, error) {
return address(nil, nil, queue, nil)
}
// PurgeQueueAddress Creates the address for purging the queue.
// See Address for more information
func PurgeQueueAddress(queue *string) (string, error) {
// See address for more information
func purgeQueueAddress(queue *string) (string, error) {
parameter := "/messages"
return Address(nil, nil, queue, &parameter)
return address(nil, nil, queue, &parameter)
}
// encodePathSegments takes a string and returns its percent-encoded representation.
@ -112,6 +171,9 @@ func bindingPathWithExchangeQueueKey(toQueue bool, sourceName, destinationName,
return fmt.Sprintf(format, bindings, sourceNameEncoded, destinationType, destinationNameEncoded, keyEncoded)
}
func validateAddress(address string) bool {
return strings.HasPrefix(address, fmt.Sprintf("/%s/", exchanges)) || strings.HasPrefix(address, fmt.Sprintf("/%s/", queues))
func validateAddress(address string) error {
if strings.HasPrefix(address, fmt.Sprintf("/%s/", exchanges)) || strings.HasPrefix(address, fmt.Sprintf("/%s/", queues)) {
return nil
}
return fmt.Errorf("invalid destination address, the address should start with /%s/ or/%s/ ", exchanges, queues)
}

View File

@ -5,18 +5,18 @@ import (
. "github.com/onsi/gomega"
)
var _ = Describe("Address builder test ", func() {
var _ = Describe("address builder test ", func() {
It("With exchange, queue and key should raise and error", func() {
queue := "my_queue"
exchange := "my_exchange"
_, err := Address(&exchange, nil, &queue, nil)
_, err := address(&exchange, nil, &queue, nil)
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(Equal("exchange and queue cannot be set together"))
})
It("Without exchange and queue should raise and error", func() {
_, err := Address(nil, nil, nil, nil)
_, err := address(nil, nil, nil, nil)
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(Equal("exchange or queue must be set"))
})
@ -25,14 +25,14 @@ var _ = Describe("Address builder test ", func() {
exchange := "my_exchange"
key := "my_key"
address, err := Address(&exchange, &key, nil, nil)
address, err := address(&exchange, &key, nil, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/exchanges/my_exchange/my_key"))
})
It("With exchange should return address", func() {
exchange := "my_exchange"
address, err := Address(&exchange, nil, nil, nil)
address, err := address(&exchange, nil, nil, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/exchanges/my_exchange"))
})
@ -42,21 +42,21 @@ var _ = Describe("Address builder test ", func() {
exchange := "my_ exchange/()"
key := "my_key "
address, err := Address(&exchange, &key, nil, nil)
address, err := address(&exchange, &key, nil, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/exchanges/my_%20exchange%2F%28%29/my_key%20"))
})
It("With queue should return address", func() {
queue := "my_queue>"
address, err := Address(nil, nil, &queue, nil)
address, err := address(nil, nil, &queue, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/queues/my_queue%3E"))
})
It("With queue and urlParameters should return address", func() {
queue := "my_queue"
address, err := PurgeQueueAddress(&queue)
address, err := purgeQueueAddress(&queue)
Expect(err).To(BeNil())
Expect(address).To(Equal("/queues/my_queue/messages"))
})

View File

@ -2,6 +2,7 @@ package rabbitmq_amqp
import (
"context"
"errors"
"github.com/Azure/go-amqp"
)
@ -31,18 +32,9 @@ func (b *AMQPBinding) SourceExchange(sourceName string) {
}
}
func (b *AMQPBinding) DestinationExchange(destinationName string) {
if len(destinationName) > 0 {
b.destinationName = destinationName
b.toQueue = false
}
}
func (b *AMQPBinding) DestinationQueue(queueName string) {
if len(queueName) > 0 {
b.destinationName = queueName
b.toQueue = true
}
func (b *AMQPBinding) Destination(name string, isQueue bool) {
b.destinationName = name
b.toQueue = isQueue
}
// Bind creates a binding between an exchange and a queue or exchange
@ -50,11 +42,20 @@ func (b *AMQPBinding) DestinationQueue(queueName string) {
// Returns the binding path that can be used to unbind the binding.
// Given a virtual host, the binding path is unique.
func (b *AMQPBinding) Bind(ctx context.Context) (string, error) {
destination := "destination_queue"
if !b.toQueue {
destination = "destination_exchange"
}
if len(b.sourceName) == 0 || len(b.destinationName) == 0 {
return "", errors.New("source and destination names are required")
}
path := bindingPath()
kv := make(map[string]any)
kv["binding_key"] = b.bindingKey
kv["source"] = b.sourceName
kv["destination_queue"] = b.destinationName
kv[destination] = b.destinationName
kv["arguments"] = make(map[string]any)
_, err := b.management.Request(ctx, kv, path, commandPost, []int{responseCode204})
bindingPathWithExchangeQueueKey := bindingPathWithExchangeQueueKey(b.toQueue, b.sourceName, b.destinationName, b.bindingKey)

View File

@ -23,20 +23,20 @@ var _ = Describe("AMQP Bindings test ", func() {
It("AMQP Bindings between Exchange and Queue Should succeed", func() {
const exchangeName = "Exchange_AMQP Bindings between Exchange and Queue should uccess"
const queueName = "Queue_AMQP Bindings between Exchange and Queue should succeed"
exchangeInfo, err := management.DeclareExchange(context.TODO(), &ExchangeSpecification{
exchangeInfo, err := management.DeclareExchange(context.TODO(), &TopicExchangeSpecification{
Name: exchangeName,
})
Expect(err).To(BeNil())
Expect(exchangeInfo).NotTo(BeNil())
Expect(exchangeInfo.Name()).To(Equal(exchangeName))
queueInfo, err := management.DeclareQueue(context.TODO(), &QueueSpecification{
queueInfo, err := management.DeclareQueue(context.TODO(), &QuorumQueueSpecification{
Name: queueName,
})
Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil())
Expect(queueInfo.Name()).To(Equal(queueName))
bindingPath, err := management.Bind(context.TODO(), &BindingSpecification{
bindingPath, err := management.Bind(context.TODO(), &ExchangeToQueueBindingSpecification{
SourceExchange: exchangeName,
DestinationQueue: queueName,
BindingKey: "routing-key",
@ -49,4 +49,68 @@ var _ = Describe("AMQP Bindings test ", func() {
err = management.DeleteQueue(context.TODO(), queueName)
Expect(err).To(BeNil())
})
It("AMQP Bindings between Exchange and Exchange Should succeed", func() {
var exchangeName = generateName("Exchange_AMQP Bindings between Exchange and Exchange should succeed")
var exchangeName2 = generateName("Exchange_AMQP Bindings between Exchange and Exchange should succeed 2")
exchangeInfo, err := management.DeclareExchange(context.TODO(), &TopicExchangeSpecification{
Name: exchangeName,
})
Expect(err).To(BeNil())
Expect(exchangeInfo).NotTo(BeNil())
Expect(exchangeInfo.Name()).To(Equal(exchangeName))
exchangeInfo2, err := management.DeclareExchange(context.TODO(), &TopicExchangeSpecification{
Name: exchangeName2})
Expect(err).To(BeNil())
Expect(exchangeInfo2).NotTo(BeNil())
Expect(exchangeInfo2.Name()).To(Equal(exchangeName2))
bindingPath, err := management.Bind(context.TODO(), &ExchangeToExchangeBindingSpecification{
SourceExchange: exchangeName,
DestinationExchange: exchangeName2,
})
Expect(err).To(BeNil())
Expect(management.Unbind(context.TODO(), bindingPath)).To(BeNil())
Expect(management.DeleteExchange(context.TODO(), exchangeName)).To(BeNil())
Expect(management.DeleteExchange(context.TODO(), exchangeName2)).To(BeNil())
})
It("AMQP Bindings should fail if source or destinations are empty", func() {
_, err := management.Bind(context.TODO(), &ExchangeToExchangeBindingSpecification{
SourceExchange: "",
DestinationExchange: "destination",
})
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("source and destination names are required"))
_, err = management.Bind(context.TODO(), &ExchangeToExchangeBindingSpecification{
SourceExchange: "source",
DestinationExchange: "",
})
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("source and destination names are required"))
_, err = management.Bind(context.TODO(), &ExchangeToQueueBindingSpecification{
SourceExchange: "",
DestinationQueue: "destination",
})
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("source and destination names are required"))
_, err = management.Bind(context.TODO(), &ExchangeToQueueBindingSpecification{
SourceExchange: "source",
DestinationQueue: "",
})
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("source and destination names are required"))
})
It("AMQP Bindings should fail specification is nil", func() {
_, err := management.Bind(context.TODO(), nil)
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("binding specification cannot be nil"))
})
})

View File

@ -22,20 +22,40 @@ type AmqpConnection struct {
session *amqp.Session
}
func (a *AmqpConnection) Publisher(ctx context.Context, destinationAdd string, linkName string) (*Publisher, error) {
if !validateAddress(destinationAdd) {
return nil, fmt.Errorf("invalid destination address, the address should start with /%s/ or/%s/ ", exchanges, queues)
// NewPublisher creates a new Publisher that sends messages to the provided destination.
// The destination is a TargetAddress that can be a Queue or an Exchange with a routing key.
// See QueueAddress and ExchangeAddress for more information.
func (a *AmqpConnection) NewPublisher(ctx context.Context, destination TargetAddress, linkName string) (*Publisher, error) {
destinationAdd := ""
err := error(nil)
if destination != nil {
destinationAdd, err = destination.toAddress()
if err != nil {
return nil, err
}
err = validateAddress(destinationAdd)
if err != nil {
return nil, err
}
}
sender, err := a.session.NewSender(ctx, destinationAdd, createSenderLinkOptions(destinationAdd, linkName, AtLeastOnce))
if err != nil {
return nil, err
}
return newPublisher(sender), nil
return newPublisher(sender, destinationAdd != ""), nil
}
func (a *AmqpConnection) Consumer(ctx context.Context, destinationAdd string, linkName string) (*Consumer, error) {
if !validateAddress(destinationAdd) {
return nil, fmt.Errorf("invalid destination address, the address should start with /%s/ or/%s/ ", exchanges, queues)
// NewConsumer creates a new Consumer that listens to the provided destination. Destination is a QueueAddress.
func (a *AmqpConnection) NewConsumer(ctx context.Context, destination *QueueAddress, linkName string) (*Consumer, error) {
destinationAdd, err := destination.toAddress()
if err != nil {
return nil, err
}
err = validateAddress(destinationAdd)
if err != nil {
return nil, err
}
receiver, err := a.session.NewReceiver(ctx, destinationAdd, createReceiverLinkOptions(destinationAdd, linkName, AtLeastOnce))
if err != nil {

View File

@ -9,46 +9,41 @@ import (
"time"
)
var _ = Describe("Consumer tests", func() {
var _ = Describe("NewConsumer tests", func() {
It("AMQP Consumer should fail due to context cancellation", func() {
qName := generateNameWithDateTime("AMQP Consumer should fail due to context cancellation")
It("AMQP NewConsumer should fail due to context cancellation", func() {
qName := generateNameWithDateTime("AMQP NewConsumer should fail due to context cancellation")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
addr, _ := QueueAddress(&qName)
queue, err := connection.Management().DeclareQueue(context.Background(), &QueueSpecification{
Name: qName,
IsAutoDelete: false,
IsExclusive: false,
QueueType: QueueType{Quorum},
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
Expect(queue).NotTo(BeNil())
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
cancel()
_, err = connection.Consumer(ctx, addr, "test")
_, err = connection.NewConsumer(ctx, &QueueAddress{
Queue: qName,
}, "test")
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("context canceled"))
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
})
It("AMQP Consumer should ack and empty the queue", func() {
qName := generateNameWithDateTime("AMQP Consumer should ack and empty the queue")
It("AMQP NewConsumer should ack and empty the queue", func() {
qName := generateNameWithDateTime("AMQP NewConsumer should ack and empty the queue")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QueueSpecification{
Name: qName,
IsAutoDelete: false,
IsExclusive: false,
QueueType: QueueType{Quorum},
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
Expect(queue).NotTo(BeNil())
publishMessages(qName, 10)
addr, _ := QueueAddress(&qName)
consumer, err := connection.Consumer(context.Background(), addr, "test")
consumer, err := connection.NewConsumer(context.Background(), &QueueAddress{Queue: qName}, "test")
Expect(err).To(BeNil())
Expect(consumer).NotTo(BeNil())
Expect(consumer).To(BeAssignableToTypeOf(&Consumer{}))
@ -66,22 +61,18 @@ var _ = Describe("Consumer tests", func() {
Expect(connection.Close(context.Background())).To(BeNil())
})
It("AMQP Consumer should requeue the message to the queue", func() {
It("AMQP NewConsumer should requeue the message to the queue", func() {
qName := generateNameWithDateTime("AMQP Consumer should requeue the message to the queue")
qName := generateNameWithDateTime("AMQP NewConsumer should requeue the message to the queue")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QueueSpecification{
Name: qName,
IsAutoDelete: false,
IsExclusive: false,
QueueType: QueueType{Quorum},
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
Expect(queue).NotTo(BeNil())
publishMessages(qName, 1)
addr, _ := QueueAddress(&qName)
consumer, err := connection.Consumer(context.Background(), addr, "test")
consumer, err := connection.NewConsumer(context.Background(), &QueueAddress{Queue: qName}, "test")
Expect(err).To(BeNil())
Expect(consumer).NotTo(BeNil())
Expect(consumer).To(BeAssignableToTypeOf(&Consumer{}))
@ -97,22 +88,18 @@ var _ = Describe("Consumer tests", func() {
Expect(connection.Close(context.Background())).To(BeNil())
})
It("AMQP Consumer should requeue the message to the queue with annotations", func() {
It("AMQP NewConsumer should requeue the message to the queue with annotations", func() {
qName := generateNameWithDateTime("AMQP Consumer should requeue the message to the queue with annotations")
qName := generateNameWithDateTime("AMQP NewConsumer should requeue the message to the queue with annotations")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QueueSpecification{
Name: qName,
IsAutoDelete: false,
IsExclusive: false,
QueueType: QueueType{Quorum},
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
Expect(queue).NotTo(BeNil())
publishMessages(qName, 1)
addr, _ := QueueAddress(&qName)
consumer, err := connection.Consumer(context.Background(), addr, "test")
consumer, err := connection.NewConsumer(context.Background(), &QueueAddress{Queue: qName}, "test")
Expect(err).To(BeNil())
Expect(consumer).NotTo(BeNil())
Expect(consumer).To(BeAssignableToTypeOf(&Consumer{}))
@ -136,22 +123,18 @@ var _ = Describe("Consumer tests", func() {
Expect(connection.Close(context.Background())).To(BeNil())
})
It("AMQP Consumer should discard the message to the queue with and without annotations", func() {
It("AMQP NewConsumer should discard the message to the queue with and without annotations", func() {
// TODO: Implement this test with a dead letter queue to test the discard feature
qName := generateNameWithDateTime("AMQP Consumer should discard the message to the queue with and without annotations")
qName := generateNameWithDateTime("AMQP NewConsumer should discard the message to the queue with and without annotations")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QueueSpecification{
Name: qName,
IsAutoDelete: false,
IsExclusive: false,
QueueType: QueueType{Quorum},
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
Expect(queue).NotTo(BeNil())
publishMessages(qName, 2)
addr, _ := QueueAddress(&qName)
consumer, err := connection.Consumer(context.Background(), addr, "test")
consumer, err := connection.NewConsumer(context.Background(), &QueueAddress{Queue: qName}, "test")
Expect(err).To(BeNil())
Expect(consumer).NotTo(BeNil())
Expect(consumer).To(BeAssignableToTypeOf(&Consumer{}))

View File

@ -2,6 +2,7 @@ package rabbitmq_amqp
import (
"context"
"errors"
"github.com/Azure/go-amqp"
)
@ -34,7 +35,11 @@ func newAmqpExchange(management *AmqpManagement, name string) *AmqpExchange {
}
func (e *AmqpExchange) Declare(ctx context.Context) (*AmqpExchangeInfo, error) {
path, err := ExchangeAddress(&e.name, nil)
if len(e.name) == 0 {
return nil, errors.New("exchange name cannot be empty")
}
path, err := exchangeAddress(&e.name, nil)
if err != nil {
return nil, err
}
@ -59,7 +64,7 @@ func (e *AmqpExchange) IsAutoDelete() bool {
}
func (e *AmqpExchange) Delete(ctx context.Context) error {
path, err := ExchangeAddress(&e.name, nil)
path, err := exchangeAddress(&e.name, nil)
if err != nil {
return err
}

View File

@ -23,7 +23,7 @@ var _ = Describe("AMQP Exchange test ", func() {
It("AMQP Exchange Declare with Default and Delete should succeed", func() {
const exchangeName = "AMQP Exchange Declare and Delete with Default should succeed"
exchangeInfo, err := management.DeclareExchange(context.TODO(), &ExchangeSpecification{
exchangeInfo, err := management.DeclareExchange(context.TODO(), &DirectExchangeSpecification{
Name: exchangeName,
})
Expect(err).To(BeNil())
@ -35,9 +35,8 @@ var _ = Describe("AMQP Exchange test ", func() {
It("AMQP Exchange Declare with Topic and Delete should succeed", func() {
const exchangeName = "AMQP Exchange Declare with Topic and Delete should succeed"
exchangeInfo, err := management.DeclareExchange(context.TODO(), &ExchangeSpecification{
Name: exchangeName,
ExchangeType: ExchangeType{Topic},
exchangeInfo, err := management.DeclareExchange(context.TODO(), &TopicExchangeSpecification{
Name: exchangeName,
})
Expect(err).To(BeNil())
Expect(exchangeInfo).NotTo(BeNil())
@ -48,10 +47,8 @@ var _ = Describe("AMQP Exchange test ", func() {
It("AMQP Exchange Declare with FanOut and Delete should succeed", func() {
const exchangeName = "AMQP Exchange Declare with FanOut and Delete should succeed"
//exchangeSpec := management.Exchange(exchangeName).ExchangeType(ExchangeType{FanOut})
exchangeInfo, err := management.DeclareExchange(context.TODO(), &ExchangeSpecification{
Name: exchangeName,
ExchangeType: ExchangeType{FanOut},
exchangeInfo, err := management.DeclareExchange(context.TODO(), &FanOutExchangeSpecification{
Name: exchangeName,
})
Expect(err).To(BeNil())
Expect(exchangeInfo).NotTo(BeNil())
@ -59,4 +56,19 @@ var _ = Describe("AMQP Exchange test ", func() {
err = management.DeleteExchange(context.TODO(), exchangeName)
Expect(err).To(BeNil())
})
It("AMQP Exchange should fail when specification is nil", func() {
_, err := management.DeclareExchange(context.TODO(), nil)
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("exchange specification cannot be nil"))
})
It("AMQP Exchange should fail when name is empty", func() {
_, err := management.DeclareExchange(context.TODO(), &TopicExchangeSpecification{
Name: "",
IsAutoDelete: false,
})
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("exchange name cannot be empty"))
})
})

View File

@ -170,23 +170,17 @@ func (a *AmqpManagement) request(ctx context.Context, id string, body any, path
return make(map[string]any), nil
}
func (a *AmqpManagement) DeclareQueue(ctx context.Context, specification *QueueSpecification) (*AmqpQueueInfo, error) {
var amqpQueue *AmqpQueue
if specification == nil || len(specification.Name) <= 0 {
// If the specification is nil or the name is empty, then we create a new queue
// with a random name with generateNameWithDefaultPrefix()
amqpQueue = newAmqpQueue(a, "")
} else {
amqpQueue = newAmqpQueue(a, specification.Name)
amqpQueue.AutoDelete(specification.IsAutoDelete)
amqpQueue.Exclusive(specification.IsExclusive)
amqpQueue.MaxLengthBytes(specification.MaxLengthBytes)
amqpQueue.DeadLetterExchange(specification.DeadLetterExchange)
amqpQueue.DeadLetterRoutingKey(specification.DeadLetterRoutingKey)
amqpQueue.QueueType(specification.QueueType)
func (a *AmqpManagement) DeclareQueue(ctx context.Context, specification QueueSpecification) (*AmqpQueueInfo, error) {
if specification == nil {
return nil, fmt.Errorf("queue specification cannot be nil. You need to provide a valid QueueSpecification")
}
amqpQueue := newAmqpQueue(a, specification.name())
amqpQueue.AutoDelete(specification.isAutoDelete())
amqpQueue.Exclusive(specification.isExclusive())
amqpQueue.QueueType(specification.queueType())
amqpQueue.SetArguments(specification.buildArguments())
return amqpQueue.Declare(ctx)
}
@ -195,14 +189,14 @@ func (a *AmqpManagement) DeleteQueue(ctx context.Context, name string) error {
return q.Delete(ctx)
}
func (a *AmqpManagement) DeclareExchange(ctx context.Context, exchangeSpecification *ExchangeSpecification) (*AmqpExchangeInfo, error) {
func (a *AmqpManagement) DeclareExchange(ctx context.Context, exchangeSpecification ExchangeSpecification) (*AmqpExchangeInfo, error) {
if exchangeSpecification == nil {
return nil, fmt.Errorf("exchangeSpecification is nil")
return nil, errors.New("exchange specification cannot be nil. You need to provide a valid ExchangeSpecification")
}
exchange := newAmqpExchange(a, exchangeSpecification.Name)
exchange.AutoDelete(exchangeSpecification.IsAutoDelete)
exchange.ExchangeType(exchangeSpecification.ExchangeType)
exchange := newAmqpExchange(a, exchangeSpecification.name())
exchange.AutoDelete(exchangeSpecification.isAutoDelete())
exchange.ExchangeType(exchangeSpecification.exchangeType())
return exchange.Declare(ctx)
}
@ -211,12 +205,15 @@ func (a *AmqpManagement) DeleteExchange(ctx context.Context, name string) error
return e.Delete(ctx)
}
func (a *AmqpManagement) Bind(ctx context.Context, bindingSpecification *BindingSpecification) (string, error) {
func (a *AmqpManagement) Bind(ctx context.Context, bindingSpecification BindingSpecification) (string, error) {
if bindingSpecification == nil {
return "", fmt.Errorf("binding specification cannot be nil. You need to provide a valid BindingSpecification")
}
bind := newAMQPBinding(a)
bind.SourceExchange(bindingSpecification.SourceExchange)
bind.DestinationQueue(bindingSpecification.DestinationQueue)
bind.DestinationExchange(bindingSpecification.DestinationExchange)
bind.BindingKey(bindingSpecification.BindingKey)
bind.SourceExchange(bindingSpecification.sourceExchange())
bind.Destination(bindingSpecification.destination(), bindingSpecification.isDestinationQueue())
bind.BindingKey(bindingSpecification.bindingKey())
return bind.Bind(ctx)
}
@ -225,7 +222,7 @@ func (a *AmqpManagement) Unbind(ctx context.Context, bindingPath string) error {
return bind.Unbind(ctx, bindingPath)
}
func (a *AmqpManagement) QueueInfo(ctx context.Context, queueName string) (*AmqpQueueInfo, error) {
path, err := QueueAddress(&queueName)
path, err := queueAddress(&queueName)
if err != nil {
return nil, err
}

View File

@ -2,6 +2,7 @@ package rabbitmq_amqp
import (
"context"
"fmt"
"github.com/Azure/go-amqp"
)
@ -10,23 +11,63 @@ type PublishResult struct {
Message *amqp.Message
}
// Publisher is a publisher that sends messages to a specific destination address.
type Publisher struct {
sender *amqp.Sender
sender *amqp.Sender
staticTargetAddress bool
}
func newPublisher(sender *amqp.Sender) *Publisher {
return &Publisher{sender: sender}
func newPublisher(sender *amqp.Sender, staticTargetAddress bool) *Publisher {
return &Publisher{sender: sender, staticTargetAddress: staticTargetAddress}
}
// Publish sends a message to the destination address.
// The message is sent to the destination address and the outcome of the operation is returned.
// The outcome is a DeliveryState that indicates if the message was accepted or rejected.
// RabbitMQ supports the following DeliveryState types:
// - StateAccepted
// - StateReleased
// - StateRejected
// See: https://www.rabbitmq.com/docs/next/amqp#outcomes for more information.
/*
Publish sends a message to the destination address that can be decided during the creation of the publisher or at the time of sending the message.
The message is sent and the outcome of the operation is returned.
The outcome is a DeliveryState that indicates if the message was accepted or rejected.
RabbitMQ supports the following DeliveryState types:
- StateAccepted
- StateReleased
- StateRejected
See: https://www.rabbitmq.com/docs/next/amqp#outcomes for more information.
Note: If the destination address is not defined during the creation, the message must have a TO property set.
You can use the helper "MessageToAddressHelper" to create the destination address.
See the examples:
Create a new publisher that sends messages to a specific destination address:
<code>
publisher, err := amqpConnection.NewPublisher(context.Background(), &rabbitmq_amqp.ExchangeAddress{
Exchange: "myExchangeName",
Key: "myRoutingKey",
}
.. publisher.Publish(context.Background(), amqp.NewMessage([]byte("Hello, World!")))
</code>
Create a new publisher that sends messages based on message destination address:
<code>
publisher, err := connection.NewPublisher(context.Background(), nil, "test")
msg := amqp.NewMessage([]byte("hello"))
..:= MessageToAddressHelper(msg, &QueueAddress{Queue: "myQueueName"})
..:= publisher.Publish(context.Background(), msg)
</code>
*/
func (m *Publisher) Publish(ctx context.Context, message *amqp.Message) (*PublishResult, error) {
if !m.staticTargetAddress {
if message.Properties == nil || message.Properties.To == nil {
return nil, fmt.Errorf("message properties TO is required to send a message to a dynamic target address")
}
err := validateAddress(*message.Properties.To)
if err != nil {
return nil, err
}
}
r, err := m.sender.SendWithReceipt(ctx, message, nil)
if err != nil {
return nil, err

View File

@ -8,18 +8,17 @@ import (
)
var _ = Describe("AMQP publisher ", func() {
It("Send a message to a queue with a Message Target Publisher", func() {
qName := generateNameWithDateTime("Send a message to a queue with a Message Target Publisher")
It("Send a message to a queue with a Message Target NewPublisher", func() {
qName := generateNameWithDateTime("Send a message to a queue with a Message Target NewPublisher")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &QueueSpecification{
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil())
dest, _ := QueueAddress(&qName)
publisher, err := connection.Publisher(context.Background(), dest, "test")
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, "test")
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
Expect(publisher).To(BeAssignableToTypeOf(&Publisher{}))
@ -36,48 +35,34 @@ var _ = Describe("AMQP publisher ", func() {
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())
})
It("Publisher should fail to a not existing exchange", func() {
It("NewPublisher should fail to a not existing exchange", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
exchangeName := "Nope"
addr, err := ExchangeAddress(&exchangeName, nil)
Expect(err).To(BeNil())
publisher, err := connection.Publisher(context.Background(), addr, "test")
publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{Exchange: exchangeName}, "test")
Expect(err).NotTo(BeNil())
Expect(publisher).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
})
It("Publisher should fail if the destination address does not start in the correct way", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
destinationAddress := "this is not valid since does not start with exchanges or queues"
Expect(err).To(BeNil())
publisher, err := connection.Publisher(context.Background(), destinationAddress, "test")
Expect(err).NotTo(BeNil())
Expect(publisher).To(BeNil())
Expect(err.Error()).To(ContainSubstring("invalid destination address"))
Expect(connection.Close(context.Background())).To(BeNil())
})
It("publishResult should released to a not existing routing key", func() {
eName := generateNameWithDateTime("publishResult should released to a not existing routing key")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
exchange, err := connection.Management().DeclareExchange(context.Background(), &ExchangeSpecification{
exchange, err := connection.Management().DeclareExchange(context.Background(), &TopicExchangeSpecification{
Name: eName,
IsAutoDelete: false,
ExchangeType: ExchangeType{Type: Topic},
})
Expect(err).To(BeNil())
Expect(exchange).NotTo(BeNil())
routingKeyNope := "I don't exist"
addr, err := ExchangeAddress(&eName, &routingKeyNope)
Expect(err).To(BeNil())
publisher, err := connection.Publisher(context.Background(), addr, "test")
publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{
Exchange: eName,
Key: routingKeyNope,
}, "test")
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
publishResult, err := publisher.Publish(context.Background(), amqp.NewMessage([]byte("hello")))
@ -93,12 +78,11 @@ var _ = Describe("AMQP publisher ", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
_, err = connection.Management().DeclareQueue(context.Background(), &QueueSpecification{
_, err = connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
dest, _ := QueueAddress(&qName)
publisher, err := connection.Publisher(context.Background(), dest, "test")
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, "test")
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
publishResult, err := publisher.Publish(context.Background(), amqp.NewMessage([]byte("hello")))
@ -110,4 +94,104 @@ var _ = Describe("AMQP publisher ", func() {
Expect(err).NotTo(BeNil())
Expect(connection.Close(context.Background()))
})
It("Multi Targets Publisher should fail with StateReleased when the destination does not exist", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
publisher, err := connection.NewPublisher(context.Background(), nil, "test")
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
qName := generateNameWithDateTime("Targets Publisher should fail when the destination does not exist")
msg := amqp.NewMessage([]byte("hello"))
Expect(MessageToAddressHelper(msg, &QueueAddress{Queue: qName})).To(BeNil())
publishResult, err := publisher.Publish(context.Background(), msg)
Expect(err).To(BeNil())
Expect(publishResult).NotTo(BeNil())
Expect(publishResult.Outcome).To(Equal(&amqp.StateReleased{}))
Expect(connection.Close(context.Background())).To(BeNil())
})
It("Multi Targets Publisher should success with StateReceived when the destination exists", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
Expect(err).To(BeNil())
publisher, err := connection.NewPublisher(context.Background(), nil, "test")
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
name := generateNameWithDateTime("Targets Publisher should success with StateReceived when the destination exists")
_, err = connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: name,
})
Expect(err).To(BeNil())
// as first message is sent to a queue, the outcome should be StateReceived
// since the message was accepted by the existing queue
msg := amqp.NewMessage([]byte("hello"))
Expect(MessageToAddressHelper(msg, &QueueAddress{Queue: name})).To(BeNil())
publishResult, err := publisher.Publish(context.Background(), msg)
Expect(err).To(BeNil())
Expect(publishResult).NotTo(BeNil())
Expect(publishResult.Outcome).To(Equal(&amqp.StateAccepted{}))
_, err = connection.Management().DeclareExchange(context.Background(), &TopicExchangeSpecification{
Name: name,
IsAutoDelete: false,
})
msg = amqp.NewMessage([]byte("hello"))
Expect(MessageToAddressHelper(msg, &ExchangeAddress{Exchange: name})).To(BeNil())
// the status should be StateReleased since the exchange does not have any binding
publishResult, err = publisher.Publish(context.Background(), msg)
Expect(err).To(BeNil())
Expect(publishResult).NotTo(BeNil())
Expect(publishResult.Outcome).To(Equal(&amqp.StateReleased{}))
// Create the binding between the exchange and the queue
_, err = connection.Management().Bind(context.Background(), &ExchangeToQueueBindingSpecification{
SourceExchange: name,
DestinationQueue: name,
BindingKey: "#",
})
Expect(err).To(BeNil())
// the status should be StateAccepted since the exchange has a binding
msg = amqp.NewMessage([]byte("hello"))
Expect(MessageToAddressHelper(msg, &ExchangeAddress{Exchange: name})).To(BeNil())
publishResult, err = publisher.Publish(context.Background(), msg)
Expect(err).To(BeNil())
Expect(publishResult).NotTo(BeNil())
Expect(publishResult.Outcome).To(Equal(&amqp.StateAccepted{}))
Expect(connection.Management().DeleteQueue(context.Background(), name)).To(BeNil())
Expect(connection.Management().DeleteExchange(context.Background(), name)).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
})
It("Multi Targets Publisher should fail it TO is not set or not valid", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
publisher, err := connection.NewPublisher(context.Background(), nil, "test")
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
msg := amqp.NewMessage([]byte("hello"))
// the message should fail since the TO property is not set
publishResult, err := publisher.Publish(context.Background(), msg)
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("message properties TO is required"))
Expect(publishResult).To(BeNil())
invalid := "invalid"
// the message should fail since the TO property is not valid
msg.Properties = &amqp.MessageProperties{
To: &invalid,
}
publishResult, err = publisher.Publish(context.Background(), msg)
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("invalid destination address"))
Expect(publishResult).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
})
})

View File

@ -2,6 +2,7 @@ package rabbitmq_amqp
import (
"context"
"errors"
"github.com/Azure/go-amqp"
)
@ -70,22 +71,8 @@ type AmqpQueue struct {
name string
}
func (a *AmqpQueue) DeadLetterExchange(dlx string) {
if len(dlx) != 0 {
a.arguments["x-dead-letter-exchange"] = dlx
}
}
func (a *AmqpQueue) DeadLetterRoutingKey(dlrk string) {
if len(dlrk) != 0 {
a.arguments["x-dead-letter-routing-key"] = dlrk
}
}
func (a *AmqpQueue) MaxLengthBytes(length int64) {
if length != 0 {
a.arguments["max-length-bytes"] = length
}
func (a *AmqpQueue) SetArguments(arguments map[string]any) {
a.arguments = arguments
}
func (a *AmqpQueue) QueueType(queueType QueueType) {
@ -94,13 +81,6 @@ func (a *AmqpQueue) QueueType(queueType QueueType) {
}
}
func (a *AmqpQueue) GetQueueType() TQueueType {
if a.arguments["x-queue-type"] == nil {
return Classic
}
return TQueueType(a.arguments["x-queue-type"].(string))
}
func (a *AmqpQueue) Exclusive(isExclusive bool) {
a.isExclusive = isExclusive
}
@ -124,32 +104,40 @@ func newAmqpQueue(management *AmqpManagement, queueName string) *AmqpQueue {
}
func (a *AmqpQueue) validate() error {
if a.arguments["max-length-bytes"] != nil {
err := validatePositive("max length", a.arguments["max-length-bytes"].(int64))
if a.arguments["x-max-length-bytes"] != nil {
err := validatePositive("max length", a.arguments["x-max-length-bytes"].(int64))
if err != nil {
return err
}
}
if a.arguments["x-max-length"] != nil {
err := validatePositive("max length", a.arguments["x-max-length"].(int64))
if err != nil {
return err
}
}
if a.arguments["x-max-priority"] != nil {
err := validatePositive("max priority", a.arguments["x-max-priority"].(int64))
if err != nil {
return err
}
}
return nil
}
func (a *AmqpQueue) Declare(ctx context.Context) (*AmqpQueueInfo, error) {
if Quorum == a.GetQueueType() ||
Stream == a.GetQueueType() {
// mandatory arguments for quorum queues and streams
a.Exclusive(false)
a.AutoDelete(false)
}
if err := a.validate(); err != nil {
return nil, err
}
if a.name == "" {
a.name = generateNameWithDefaultPrefix()
return nil, errors.New("queue name is required")
}
path, err := QueueAddress(&a.name)
path, err := queueAddress(&a.name)
if err != nil {
return nil, err
}
@ -166,7 +154,7 @@ func (a *AmqpQueue) Declare(ctx context.Context) (*AmqpQueueInfo, error) {
}
func (a *AmqpQueue) Delete(ctx context.Context) error {
path, err := QueueAddress(&a.name)
path, err := queueAddress(&a.name)
if err != nil {
return err
}
@ -175,7 +163,7 @@ func (a *AmqpQueue) Delete(ctx context.Context) error {
}
func (a *AmqpQueue) Purge(ctx context.Context) (int, error) {
path, err := PurgeQueueAddress(&a.name)
path, err := purgeQueueAddress(&a.name)
if err != nil {
return 0, err
}

View File

@ -25,8 +25,8 @@ var _ = Describe("AMQP Queue test ", func() {
})
It("AMQP Queue Declare With Response and Get/Delete should succeed", func() {
const queueName = "AMQP Queue Declare With Response and Delete should succeed"
queueInfo, err := management.DeclareQueue(context.TODO(), &QueueSpecification{
var queueName = generateName("AMQP Queue Declare With Response and Delete should succeed")
queueInfo, err := management.DeclareQueue(context.TODO(), &QuorumQueueSpecification{
Name: queueName,
})
Expect(err).To(BeNil())
@ -35,7 +35,7 @@ var _ = Describe("AMQP Queue test ", func() {
Expect(queueInfo.IsDurable()).To(BeTrue())
Expect(queueInfo.IsAutoDelete()).To(BeFalse())
Expect(queueInfo.IsExclusive()).To(BeFalse())
Expect(queueInfo.Type()).To(Equal(Classic))
Expect(queueInfo.Type()).To(Equal(Quorum))
// validate GET (query queue info)
queueInfoReceived, err := management.QueueInfo(context.TODO(), queueName)
@ -46,16 +46,22 @@ var _ = Describe("AMQP Queue test ", func() {
})
It("AMQP Queue Declare With Parameters and Get/Delete should succeed", func() {
const queueName = "AMQP Queue Declare With Parameters and Delete should succeed"
var queueName = generateName("AMQP Queue Declare With Parameters and Delete should succeed")
queueInfo, err := management.DeclareQueue(context.TODO(), &QueueSpecification{
queueInfo, err := management.DeclareQueue(context.TODO(), &ClassicQueueSpecification{
Name: queueName,
IsAutoDelete: true,
IsExclusive: true,
QueueType: QueueType{Classic},
MaxLengthBytes: CapacityGB(1),
AutoExpire: 1000,
MessageTTL: 1000,
OverflowStrategy: &DropHeadOverflowStrategy{},
SingleActiveConsumer: true,
DeadLetterExchange: "dead-letter-exchange",
DeadLetterRoutingKey: "dead-letter-routing-key",
MaxLength: 9_000,
MaxLengthBytes: CapacityGB(1),
MaxPriority: 2,
LeaderLocator: &BalancedLeaderLocator{},
})
Expect(err).To(BeNil())
@ -70,7 +76,14 @@ var _ = Describe("AMQP Queue test ", func() {
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-dead-letter-exchange", "dead-letter-exchange"))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-dead-letter-routing-key", "dead-letter-routing-key"))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("max-length-bytes", int64(1000000000)))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-max-length-bytes", int64(1000000000)))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-max-length", int64(9000)))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-message-ttl", int64(1000)))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-single-active-consumer", true))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-overflow", "drop-head"))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-expires", int64(1000)))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-max-priority", int64(2)))
Expect(queueInfo.Arguments()).To(HaveKeyWithValue("x-queue-leader-locator", "random"))
// validate GET (query queue info)
queueInfoReceived, err := management.QueueInfo(context.TODO(), queueName)
@ -82,14 +95,11 @@ var _ = Describe("AMQP Queue test ", func() {
})
It("AMQP Declare Quorum Queue and Get/Delete should succeed", func() {
const queueName = "AMQP Declare Quorum Queue and Delete should succeed"
var queueName = generateName("AMQP Declare Quorum Queue and Delete should succeed")
// Quorum queue will ignore Exclusive and AutoDelete settings
// since they are not supported by quorum queues
queueInfo, err := management.DeclareQueue(context.TODO(), &QueueSpecification{
Name: queueName,
IsAutoDelete: true,
IsExclusive: true,
QueueType: QueueType{Quorum},
queueInfo, err := management.DeclareQueue(context.TODO(), &QuorumQueueSpecification{
Name: queueName,
})
Expect(err).To(BeNil())
@ -113,12 +123,10 @@ var _ = Describe("AMQP Queue test ", func() {
// Stream queue will ignore Exclusive and AutoDelete settings
// since they are not supported by quorum queues
queueInfo, err := management.DeclareQueue(context.TODO(), &QueueSpecification{
queueInfo, err := management.DeclareQueue(context.TODO(), &ClassicQueueSpecification{
Name: queueName,
IsAutoDelete: true,
IsExclusive: true,
QueueType: QueueType{Stream},
})
IsAutoDelete: false,
IsExclusive: false})
Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil())
@ -126,7 +134,7 @@ var _ = Describe("AMQP Queue test ", func() {
Expect(queueInfo.IsDurable()).To(BeTrue())
Expect(queueInfo.IsAutoDelete()).To(BeFalse())
Expect(queueInfo.IsExclusive()).To(BeFalse())
Expect(queueInfo.Type()).To(Equal(Stream))
Expect(queueInfo.Type()).To(Equal(Classic))
// validate GET (query queue info)
queueInfoReceived, err := management.QueueInfo(context.TODO(), queueName)
Expect(queueInfoReceived).To(Equal(queueInfo))
@ -136,29 +144,18 @@ var _ = Describe("AMQP Queue test ", func() {
})
It("AMQP Declare Queue with invalid type should fail", func() {
const queueName = "AMQP Declare Queue with invalid type should fail"
_, err := management.DeclareQueue(context.TODO(), &QueueSpecification{
Name: queueName,
QueueType: QueueType{Type: "invalid"},
})
Expect(err).NotTo(BeNil())
})
It("AMQP Declare Queue should fail with Precondition fail", func() {
// The first queue is declared as Classic, and it should succeed
// The second queue is declared as Quorum, and it should fail since it is already declared as Classic
queueName := generateName("AMQP Declare Queue should fail with Precondition fail")
_, err := management.DeclareQueue(context.TODO(), &QueueSpecification{
Name: queueName,
QueueType: QueueType{Classic},
//queueName := generateName("AMQP Declare Queue should fail with Precondition fail")
queueName := "ab"
_, err := management.DeclareQueue(context.TODO(), &ClassicQueueSpecification{
Name: queueName,
})
Expect(err).To(BeNil())
_, err = management.DeclareQueue(context.TODO(), &QueueSpecification{
Name: queueName,
QueueType: QueueType{Quorum},
_, err = management.DeclareQueue(context.TODO(), &QuorumQueueSpecification{
Name: queueName,
})
Expect(err).NotTo(BeNil())
@ -169,7 +166,7 @@ var _ = Describe("AMQP Queue test ", func() {
It("AMQP Declare Queue should fail during validation", func() {
queueName := generateName("AMQP Declare Queue should fail during validation")
_, err := management.DeclareQueue(context.TODO(), &QueueSpecification{
_, err := management.DeclareQueue(context.TODO(), &QuorumQueueSpecification{
Name: queueName,
MaxLengthBytes: -1,
})
@ -178,8 +175,18 @@ var _ = Describe("AMQP Queue test ", func() {
Expect(err).To(HaveOccurred())
})
It("AMQP Declare Queue should fail if queue specification is nil", func() {
_, err := management.DeclareQueue(context.TODO(), nil)
Expect(err).NotTo(BeNil())
Expect(err).To(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("queue specification cannot be nil"))
})
It("AMQP Declare Queue should create client name queue", func() {
queueInfo, err := management.DeclareQueue(context.TODO(), nil)
queueInfo, err := management.DeclareQueue(context.TODO(), &AutoGeneratedQueueSpecification{
IsAutoDelete: true,
IsExclusive: false,
})
Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil())
Expect(queueInfo.Name()).To(ContainSubstring("client.gen-"))
@ -189,7 +196,7 @@ var _ = Describe("AMQP Queue test ", func() {
It("AMQP Purge Queue should succeed and return the number of messages purged", func() {
queueName := generateName("AMQP Purge Queue should succeed and return the number of messages purged")
queueInfo, err := management.DeclareQueue(context.TODO(), &QueueSpecification{
queueInfo, err := management.DeclareQueue(context.TODO(), &QuorumQueueSpecification{
Name: queueName,
})
Expect(err).To(BeNil())
@ -213,10 +220,7 @@ func publishMessages(queueName string, count int) {
conn, err := Dial(context.TODO(), []string{"amqp://guest:guest@localhost"}, nil)
Expect(err).To(BeNil())
address, err := QueueAddress(&queueName)
Expect(err).To(BeNil())
publisher, err := conn.Publisher(context.TODO(), address, "test")
publisher, err := conn.NewPublisher(context.TODO(), &QueueAddress{Queue: queueName}, "test")
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())

View File

@ -16,17 +16,264 @@ func (e QueueType) String() string {
return string(e.Type)
}
// QueueSpecification represents the specification of a queue
type QueueSpecification struct {
type QueueSpecification interface {
name() string
isAutoDelete() bool
isExclusive() bool
queueType() QueueType
buildArguments() map[string]any
}
// QuorumQueueSpecification represents the specification of the quorum queue
type OverflowStrategy interface {
overflowStrategy() string
}
type DropHeadOverflowStrategy struct {
}
func (d *DropHeadOverflowStrategy) overflowStrategy() string {
return "drop-head"
}
type RejectPublishOverflowStrategy struct {
}
func (r *RejectPublishOverflowStrategy) overflowStrategy() string {
return "reject-publish"
}
type RejectPublishDlxOverflowStrategy struct {
}
func (r *RejectPublishDlxOverflowStrategy) overflowStrategy() string {
return "reject-publish-dlx"
}
type LeaderLocator interface {
leaderLocator() string
}
type BalancedLeaderLocator struct {
}
func (r *BalancedLeaderLocator) leaderLocator() string {
return "random"
}
type ClientLocalLeaderLocator struct {
}
func (r *ClientLocalLeaderLocator) leaderLocator() string {
return "client-local"
}
type QuorumQueueSpecification struct {
Name string
AutoExpire int64
MessageTTL int64
OverflowStrategy OverflowStrategy
SingleActiveConsumer bool
DeadLetterExchange string
DeadLetterRoutingKey string
MaxLength int64
MaxLengthBytes int64
DeliveryLimit int64
TargetClusterSize int64
LeaderLocator LeaderLocator
}
func (q *QuorumQueueSpecification) name() string {
return q.Name
}
func (q *QuorumQueueSpecification) isAutoDelete() bool {
return false
}
func (q *QuorumQueueSpecification) isExclusive() bool {
return false
}
func (q *QuorumQueueSpecification) queueType() QueueType {
return QueueType{Type: Quorum}
}
func (q *QuorumQueueSpecification) buildArguments() map[string]any {
result := map[string]any{}
if q.MaxLengthBytes != 0 {
result["x-max-length-bytes"] = q.MaxLengthBytes
}
if len(q.DeadLetterExchange) != 0 {
result["x-dead-letter-exchange"] = q.DeadLetterExchange
}
if len(q.DeadLetterRoutingKey) != 0 {
result["x-dead-letter-routing-key"] = q.DeadLetterRoutingKey
}
if q.AutoExpire != 0 {
result["x-expires"] = q.AutoExpire
}
if q.MessageTTL != 0 {
result["x-message-ttl"] = q.MessageTTL
}
if q.OverflowStrategy != nil {
result["x-overflow"] = q.OverflowStrategy.overflowStrategy()
}
if q.SingleActiveConsumer {
result["x-single-active-consumer"] = true
}
if q.MaxLength != 0 {
result["x-max-length"] = q.MaxLength
}
if q.DeliveryLimit != 0 {
result["x-delivery-limit"] = q.DeliveryLimit
}
if q.TargetClusterSize != 0 {
result["x-quorum-target-group-size"] = q.TargetClusterSize
}
if q.LeaderLocator != nil {
result["x-queue-leader-locator"] = q.LeaderLocator.leaderLocator()
}
result["x-queue-type"] = q.queueType().String()
return result
}
// ClassicQueueSpecification represents the specification of the classic queue
type ClassicQueueSpecification struct {
Name string
IsAutoDelete bool
IsExclusive bool
QueueType QueueType
MaxLengthBytes int64
AutoExpire int64
MessageTTL int64
OverflowStrategy OverflowStrategy
SingleActiveConsumer bool
DeadLetterExchange string
DeadLetterRoutingKey string
MaxLength int64
MaxLengthBytes int64
MaxPriority int64
LeaderLocator LeaderLocator
}
func (q *ClassicQueueSpecification) name() string {
return q.Name
}
func (q *ClassicQueueSpecification) isAutoDelete() bool {
return q.IsAutoDelete
}
func (q *ClassicQueueSpecification) isExclusive() bool {
return q.IsExclusive
}
func (q *ClassicQueueSpecification) queueType() QueueType {
return QueueType{Type: Classic}
}
func (q *ClassicQueueSpecification) buildArguments() map[string]any {
result := map[string]any{}
if q.MaxLengthBytes != 0 {
result["x-max-length-bytes"] = q.MaxLengthBytes
}
if len(q.DeadLetterExchange) != 0 {
result["x-dead-letter-exchange"] = q.DeadLetterExchange
}
if len(q.DeadLetterRoutingKey) != 0 {
result["x-dead-letter-routing-key"] = q.DeadLetterRoutingKey
}
if q.AutoExpire != 0 {
result["x-expires"] = q.AutoExpire
}
if q.MessageTTL != 0 {
result["x-message-ttl"] = q.MessageTTL
}
if q.OverflowStrategy != nil {
result["x-overflow"] = q.OverflowStrategy.overflowStrategy()
}
if q.SingleActiveConsumer {
result["x-single-active-consumer"] = true
}
if q.MaxLength != 0 {
result["x-max-length"] = q.MaxLength
}
if q.MaxPriority != 0 {
result["x-max-priority"] = q.MaxPriority
}
if q.LeaderLocator != nil {
result["x-queue-leader-locator"] = q.LeaderLocator.leaderLocator()
}
result["x-queue-type"] = q.queueType().String()
return result
}
type AutoGeneratedQueueSpecification struct {
IsAutoDelete bool
IsExclusive bool
MaxLength int64
MaxLengthBytes int64
}
func (a *AutoGeneratedQueueSpecification) name() string {
return generateNameWithDefaultPrefix()
}
func (a *AutoGeneratedQueueSpecification) isAutoDelete() bool {
return a.IsAutoDelete
}
func (a *AutoGeneratedQueueSpecification) isExclusive() bool {
return a.IsExclusive
}
func (a *AutoGeneratedQueueSpecification) queueType() QueueType {
return QueueType{Classic}
}
func (a *AutoGeneratedQueueSpecification) buildArguments() map[string]any {
result := map[string]any{}
if a.MaxLengthBytes != 0 {
result["x-max-length-bytes"] = a.MaxLengthBytes
}
if a.MaxLength != 0 {
result["x-max-length"] = a.MaxLength
}
result["x-queue-type"] = a.queueType().String()
return result
}
// / **** Exchange ****
// TExchangeType represents the type of exchange
type TExchangeType string
const (
@ -43,15 +290,124 @@ func (e ExchangeType) String() string {
return string(e.Type)
}
type ExchangeSpecification struct {
Name string
IsAutoDelete bool
ExchangeType ExchangeType
// ExchangeSpecification represents the specification of an exchange
type ExchangeSpecification interface {
name() string
isAutoDelete() bool
exchangeType() ExchangeType
buildArguments() map[string]any
}
type BindingSpecification struct {
type DirectExchangeSpecification struct {
Name string
IsAutoDelete bool
}
func (d *DirectExchangeSpecification) name() string {
return d.Name
}
func (d *DirectExchangeSpecification) isAutoDelete() bool {
return d.IsAutoDelete
}
func (d *DirectExchangeSpecification) exchangeType() ExchangeType {
return ExchangeType{Type: Direct}
}
func (d *DirectExchangeSpecification) buildArguments() map[string]any {
return map[string]any{}
}
type TopicExchangeSpecification struct {
Name string
IsAutoDelete bool
}
func (t *TopicExchangeSpecification) name() string {
return t.Name
}
func (t *TopicExchangeSpecification) isAutoDelete() bool {
return t.IsAutoDelete
}
func (t *TopicExchangeSpecification) exchangeType() ExchangeType {
return ExchangeType{Type: Topic}
}
func (t *TopicExchangeSpecification) buildArguments() map[string]any {
return map[string]any{}
}
type FanOutExchangeSpecification struct {
Name string
IsAutoDelete bool
}
func (f *FanOutExchangeSpecification) name() string {
return f.Name
}
func (f *FanOutExchangeSpecification) isAutoDelete() bool {
return f.IsAutoDelete
}
func (f *FanOutExchangeSpecification) exchangeType() ExchangeType {
return ExchangeType{Type: FanOut}
}
func (f *FanOutExchangeSpecification) buildArguments() map[string]any {
return map[string]any{}
}
type BindingSpecification interface {
sourceExchange() string
destination() string
bindingKey() string
isDestinationQueue() bool
}
type ExchangeToQueueBindingSpecification struct {
SourceExchange string
DestinationQueue string
BindingKey string
}
func (e *ExchangeToQueueBindingSpecification) sourceExchange() string {
return e.SourceExchange
}
func (e *ExchangeToQueueBindingSpecification) destination() string {
return e.DestinationQueue
}
func (e *ExchangeToQueueBindingSpecification) isDestinationQueue() bool {
return true
}
func (e *ExchangeToQueueBindingSpecification) bindingKey() string {
return e.BindingKey
}
type ExchangeToExchangeBindingSpecification struct {
SourceExchange string
DestinationQueue string
DestinationExchange string
BindingKey string
}
func (e *ExchangeToExchangeBindingSpecification) sourceExchange() string {
return e.SourceExchange
}
func (e *ExchangeToExchangeBindingSpecification) destination() string {
return e.DestinationExchange
}
func (e *ExchangeToExchangeBindingSpecification) isDestinationQueue() bool {
return false
}
func (e *ExchangeToExchangeBindingSpecification) bindingKey() string {
return e.BindingKey
}