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}