use std::future::{Future, ready};
use std::num::NonZeroU32;
use std::time::Duration;
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::Bindings;
use ruststream::{
AddressedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource, nonzero,
};
#[cfg(feature = "asyncapi")]
use crate::bindings;
use crate::broker::ConnectedAmqpBroker;
use crate::error::AmqpError;
use crate::subscriber::AmqpSubscriber;
pub const DEFAULT_CREDIT: NonZeroU32 = nonzero!(256);
pub const DEFAULT_BATCH_WAIT: Duration = Duration::from_millis(10);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Settle {
#[default]
AtLeastOnce,
AtMostOnce,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Queue,
Topic,
Raw,
}
#[cfg(feature = "testing")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Routing {
Anycast,
Multicast,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct AmqpAddress {
address: String,
kind: Kind,
credit: NonZeroU32,
settle: Settle,
batch_wait: Duration,
}
impl AmqpAddress {
fn of(address: String, kind: Kind) -> Self {
Self {
address,
kind,
credit: DEFAULT_CREDIT,
settle: Settle::default(),
batch_wait: DEFAULT_BATCH_WAIT,
}
}
pub fn queue(name: impl Into<String>) -> Self {
Self::of(name.into(), Kind::Queue)
}
pub fn topic(name: impl Into<String>) -> Self {
Self::of(name.into(), Kind::Topic)
}
pub fn raw(address: impl Into<String>) -> Self {
Self::of(address.into(), Kind::Raw)
}
pub fn credit(mut self, credit: NonZeroU32) -> Self {
self.credit = credit;
self
}
pub fn settle(mut self, settle: Settle) -> Self {
self.settle = settle;
self
}
pub fn batch_wait(mut self, batch_wait: Duration) -> Self {
self.batch_wait = batch_wait;
self
}
#[must_use]
pub fn address(&self) -> &str {
&self.address
}
pub(crate) fn credit_value(&self) -> u32 {
self.credit.get()
}
pub(crate) fn settle_value(&self) -> Settle {
self.settle
}
pub(crate) fn batch_wait_value(&self) -> Duration {
self.batch_wait
}
fn redelivery_target(&self) -> RedeliveryAddress {
RedeliveryAddress::new(self.address.clone())
}
#[cfg(feature = "testing")]
pub(crate) fn routing(&self) -> Routing {
match self.kind {
Kind::Topic => Routing::Multicast,
Kind::Queue | Kind::Raw => Routing::Anycast,
}
}
pub(crate) fn capability(&self) -> Option<&'static str> {
match self.kind {
Kind::Queue => Some("queue"),
Kind::Topic => Some("topic"),
Kind::Raw => None,
}
}
#[cfg(feature = "asyncapi")]
fn describe_channel(&self) -> Bindings {
bindings::channel(
&self.address,
self.capability(),
self.credit.get(),
self.settle,
)
}
pub(crate) fn validate(&self) -> Result<(), AmqpError> {
if self.address.is_empty() {
return Err(AmqpError::InvalidAddress(
"address must be non-empty".into(),
));
}
Ok(())
}
}
impl SubscriptionSource<ConnectedAmqpBroker> for AmqpAddress {
type Subscriber = AmqpSubscriber;
type Copies = AddressedCopies;
fn name(&self) -> &str {
self.address()
}
async fn subscribe(self, connected: &ConnectedAmqpBroker) -> Result<AmqpSubscriber, AmqpError> {
connected.subscribe_address(self).await
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self) -> Bindings {
self.describe_channel()
}
}
impl RedeliveryAddressed<ConnectedAmqpBroker> for AmqpAddress {
fn redelivery_address(
&self,
_connected: &ConnectedAmqpBroker,
) -> impl Future<Output = Result<RedeliveryAddress, AmqpError>> + Send {
ready(Ok(self.redelivery_target()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_address_is_rejected_before_io() {
assert!(matches!(
AmqpAddress::queue("").validate(),
Err(AmqpError::InvalidAddress(_))
));
}
#[test]
fn the_credit_defaults_and_takes_an_override() {
assert_eq!(AmqpAddress::queue("orders").credit_value(), 256);
assert_eq!(
AmqpAddress::queue("orders")
.credit(nonzero!(64))
.credit_value(),
64
);
}
#[test]
fn the_batch_deadline_defaults_and_takes_an_override() {
assert_eq!(
AmqpAddress::queue("q").batch_wait_value(),
DEFAULT_BATCH_WAIT
);
assert_eq!(
AmqpAddress::queue("q")
.batch_wait(Duration::from_millis(50))
.batch_wait_value(),
Duration::from_millis(50)
);
}
#[test]
fn constructors_pick_the_matching_capability() {
assert_eq!(AmqpAddress::queue("q").capability(), Some("queue"));
assert_eq!(AmqpAddress::topic("t").capability(), Some("topic"));
assert_eq!(AmqpAddress::raw("/queues/q").capability(), None);
}
}