Skip to main content

ruststream_amqp/
address.rs

1//! [`AmqpAddress`]: the subscription descriptor.
2//!
3//! `AMQP` 1.0 standardises the wire but not the meaning of an address, so addressing stays
4//! explicit: `queue` for anycast (competing consumers), `topic` for multicast (fan-out), and
5//! `raw` for deployments with their own convention. The queue/topic constructors advertise the
6//! matching terminus capability (`"queue"` / `"topic"`), which is how `ActiveMQ` Artemis and other
7//! products disambiguate; `raw` sends the address verbatim with no capability.
8
9use ruststream::SubscriptionSource;
10
11use crate::broker::ConnectedAmqpBroker;
12use crate::error::AmqpError;
13use crate::subscriber::AmqpSubscriber;
14
15/// Default protocol-level credit (prefetch) granted to a subscription.
16pub const DEFAULT_CREDIT: u32 = 256;
17
18/// Delivery guarantee of a subscription.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum Settle {
21    /// Deliveries are settled by the handler: `ack` accepts, `nack` releases or rejects. The
22    /// default.
23    #[default]
24    AtLeastOnce,
25    /// Deliveries are settled on receipt; `ack`/`nack` report
26    /// [`AckError::Unsupported`](ruststream::AckError::Unsupported).
27    AtMostOnce,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum Kind {
32    Queue,
33    Topic,
34    Raw,
35}
36
37/// A subscription descriptor for an `AMQP` 1.0 address.
38///
39/// Implements [`SubscriptionSource`], so it can sit inline in the `#[subscriber(..)]` decorator:
40///
41/// ```
42/// use ruststream_amqp::AmqpAddress;
43///
44/// let source = AmqpAddress::queue("orders").credit(64);
45/// # let _ = source;
46/// ```
47#[derive(Debug, Clone, PartialEq, Eq)]
48#[must_use]
49pub struct AmqpAddress {
50    address: String,
51    kind: Kind,
52    credit: u32,
53    settle: Settle,
54}
55
56impl AmqpAddress {
57    fn of(address: String, kind: Kind) -> Self {
58        Self {
59            address,
60            kind,
61            credit: DEFAULT_CREDIT,
62            settle: Settle::default(),
63        }
64    }
65
66    /// An anycast address: competing consumers, each message delivered to one of them.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use ruststream_amqp::AmqpAddress;
72    /// let source = AmqpAddress::queue("orders");
73    /// # let _ = source;
74    /// ```
75    pub fn queue(name: impl Into<String>) -> Self {
76        Self::of(name.into(), Kind::Queue)
77    }
78
79    /// A multicast address: fan-out, each message delivered to every subscriber.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use ruststream_amqp::AmqpAddress;
85    /// let source = AmqpAddress::topic("events");
86    /// # let _ = source;
87    /// ```
88    pub fn topic(name: impl Into<String>) -> Self {
89        Self::of(name.into(), Kind::Topic)
90    }
91
92    /// A verbatim address, for deployments with their own addressing convention
93    /// (`"/queues/orders"` on `RabbitMQ` 4.x, a fully qualified queue on Artemis).
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use ruststream_amqp::AmqpAddress;
99    /// let source = AmqpAddress::raw("/queues/orders");
100    /// # let _ = source;
101    /// ```
102    pub fn raw(address: impl Into<String>) -> Self {
103        Self::of(address.into(), Kind::Raw)
104    }
105
106    /// Sets the protocol-level credit (prefetch): how many unsettled deliveries the broker may
107    /// have in flight to this subscription. Defaults to [`DEFAULT_CREDIT`].
108    pub fn credit(mut self, credit: u32) -> Self {
109        self.credit = credit;
110        self
111    }
112
113    /// Sets the delivery guarantee. Defaults to [`Settle::AtLeastOnce`].
114    pub fn settle(mut self, settle: Settle) -> Self {
115        self.settle = settle;
116        self
117    }
118
119    /// The address string sent to the broker.
120    #[must_use]
121    pub fn address(&self) -> &str {
122        &self.address
123    }
124
125    pub(crate) fn credit_value(&self) -> u32 {
126        self.credit
127    }
128
129    pub(crate) fn settle_value(&self) -> Settle {
130        self.settle
131    }
132
133    /// The terminus capability this descriptor advertises, when one applies.
134    pub(crate) fn capability(&self) -> Option<&'static str> {
135        match self.kind {
136            Kind::Queue => Some("queue"),
137            Kind::Topic => Some("topic"),
138            Kind::Raw => None,
139        }
140    }
141
142    /// Rejects descriptors that cannot form a subscription, before any I/O.
143    pub(crate) fn validate(&self) -> Result<(), AmqpError> {
144        if self.address.is_empty() {
145            return Err(AmqpError::InvalidAddress(
146                "address must be non-empty".into(),
147            ));
148        }
149        if self.credit == 0 {
150            return Err(AmqpError::InvalidAddress(
151                "credit must be at least 1".into(),
152            ));
153        }
154        Ok(())
155    }
156}
157
158impl SubscriptionSource<ConnectedAmqpBroker> for AmqpAddress {
159    type Subscriber = AmqpSubscriber;
160
161    fn name(&self) -> &str {
162        self.address()
163    }
164
165    async fn subscribe(self, connected: &ConnectedAmqpBroker) -> Result<AmqpSubscriber, AmqpError> {
166        connected.subscribe_address(self).await
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn empty_address_is_rejected_before_io() {
176        assert!(matches!(
177            AmqpAddress::queue("").validate(),
178            Err(AmqpError::InvalidAddress(_))
179        ));
180    }
181
182    #[test]
183    fn zero_credit_is_rejected_before_io() {
184        assert!(matches!(
185            AmqpAddress::queue("orders").credit(0).validate(),
186            Err(AmqpError::InvalidAddress(_))
187        ));
188    }
189
190    #[test]
191    fn constructors_pick_the_matching_capability() {
192        assert_eq!(AmqpAddress::queue("q").capability(), Some("queue"));
193        assert_eq!(AmqpAddress::topic("t").capability(), Some("topic"));
194        assert_eq!(AmqpAddress::raw("/queues/q").capability(), None);
195    }
196}