Skip to main content

ruststream_lapin/
publish_policy.rs

1//! The declaration half of publishing: the policies and what they pair into.
2//!
3//! A policy holds nothing but publish options, so it is constructible anywhere - in a router
4//! definition, in configuration, before anything connects. Pairing it with a
5//! [`ConnectedLapinBroker`] produces the live publisher (see [`crate::publisher`]), which is the
6//! only value with a publish surface. The publishing mode is a policy transition:
7//! [`LapinPublish::confirms`] and [`LapinPublish::server_tx`] move to the transactional policies,
8//! keeping the options.
9
10use std::future::{Future, ready};
11use std::time::Duration;
12
13#[cfg(feature = "asyncapi")]
14use ruststream::asyncapi::Bindings;
15use ruststream::{PairError, PublishPolicy};
16
17use crate::broker::ConnectedLapinBroker;
18#[cfg(feature = "testing")]
19use crate::error::AmqpError;
20#[cfg(feature = "testing")]
21use crate::in_process::Bus;
22use crate::publish_step::LapinPublishOptions;
23#[cfg(feature = "testing")]
24use crate::publisher::Buffered;
25use crate::publisher::{ConfirmsPublisher, LapinPublisher, ServerTxPublisher};
26
27use self::sealed::Sealed;
28
29pub(crate) mod sealed {
30    /// Seals [`LapinPublishPolicy`](super::LapinPublishPolicy): pairing an AMQP publisher opens
31    /// no channel of its own, and the synchronous
32    /// [`publisher`](crate::ConnectedLapinBroker::publisher) accessor depends on that.
33    pub trait Sealed {}
34
35    impl Sealed for super::LapinPublish {}
36    impl Sealed for super::ConfirmsPublish {}
37    impl Sealed for super::ServerTxPublish {}
38    impl Sealed for crate::requester::LapinRequest {}
39}
40
41/// What every `RabbitMQ` publish policy carries: where to publish, and the per-message properties
42/// a publish through it takes unless its own call site says otherwise.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub(crate) struct PublishOptions {
45    pub(crate) exchange: String,
46    pub(crate) defaults: LapinPublishOptions,
47}
48
49impl PublishOptions {
50    /// The settings one publish carries: what its call site adjusted, over the policy's defaults.
51    pub(crate) fn resolve(&self, call: Option<&LapinPublishOptions>) -> LapinPublishOptions {
52        call.map_or(self.defaults, |call| call.over(&self.defaults))
53    }
54}
55
56#[cfg(feature = "testing")]
57impl PublishOptions {
58    /// Publishes `buffered` onto the in-process transport in order, each message's settings
59    /// resolved over these, and stops at the first one the transport refuses: the messages already
60    /// published stay published, as they do when a live flush stops.
61    pub(crate) fn flush_in_process(
62        &self,
63        bus: &Bus,
64        buffered: &[Buffered],
65    ) -> Result<(), AmqpError> {
66        for entry in buffered {
67            bus.publish(
68                &self.exchange,
69                &entry.routing_key,
70                &entry.payload,
71                &entry.headers,
72                &self.resolve(Some(&entry.options)),
73            )?;
74        }
75        Ok(())
76    }
77}
78
79impl Default for PublishOptions {
80    fn default() -> Self {
81        Self {
82            exchange: String::new(),
83            defaults: LapinPublishOptions {
84                persistent: Some(true),
85                ..LapinPublishOptions::default()
86            },
87        }
88    }
89}
90
91/// Writes the four policy setters every publish policy of this crate shares.
92///
93/// They differ only in the type they return, and a policy is a newtype over [`PublishOptions`],
94/// so the bodies would be four copies each. The documentation is written once here and reaches
95/// every policy's rustdoc.
96macro_rules! publish_policy_settings {
97    ($policy:ident) => {
98        impl $policy {
99            /// What this policy hands the publisher it pairs into.
100            ///
101            /// The publishers take the value by move at `bind`; this borrow is for the document,
102            /// which reads it.
103            #[cfg(feature = "asyncapi")]
104            pub(crate) const fn publish_options(&self) -> &PublishOptions {
105                &self.0
106            }
107
108            /// Publishes to `exchange` instead of the default exchange.
109            pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
110                self.0.exchange = exchange.into();
111                self
112            }
113
114            /// Whether messages are marked persistent (delivery mode 2). Defaults to `true`.
115            ///
116            /// One message departs from this with the publish builder's
117            /// [`persistent`](crate::LapinPublishSteps::persistent) step.
118            pub fn persistent(mut self, persistent: bool) -> Self {
119                self.0.defaults.persistent = Some(persistent);
120                self
121            }
122
123            /// The AMQP `priority` property messages carry. Unset by default, so the broker sees
124            /// no priority at all.
125            ///
126            /// It only orders deliveries on a queue declared with `x-max-priority`. One message
127            /// departs from this with the publish builder's
128            /// [`priority`](crate::LapinPublishSteps::priority) step.
129            pub fn priority(mut self, priority: u8) -> Self {
130                self.0.defaults.priority = Some(priority);
131                self
132            }
133
134            /// The AMQP per-message `expiration` (TTL) messages carry: the broker drops one once
135            /// `ttl` has passed without it being consumed. Unset by default, so messages do not
136            /// expire.
137            ///
138            /// One message departs from this with the publish builder's
139            /// [`expiration`](crate::LapinPublishSteps::expiration) step.
140            pub fn expiration(mut self, ttl: Duration) -> Self {
141                self.0.defaults.expiration = Some(ttl);
142                self
143            }
144        }
145    };
146}
147
148/// Writes the three document methods every publish policy of this crate answers the same way.
149///
150/// A policy describes the channel it publishes to, the properties its messages carry, and where a
151/// client reads the address of an answer: this crate routes a reply by the `reply-to` header
152/// whichever publisher carries it. The bodies are the same for every policy, and the core copies
153/// nothing between them, so they are written once here.
154///
155/// The core names the hooks' parameter `channel`, the `AsyncAPI` word for what a message is
156/// published to. It is the routing key here, and `channel` is an AMQP connection's own word in
157/// this crate, so the parameter is `destination` on this side of the call.
158macro_rules! publish_policy_bindings {
159    () => {
160        #[cfg(feature = "asyncapi")]
161        fn channel_bindings(&self, destination: &str) -> Bindings {
162            crate::bindings::publish_channel(self.publish_options(), destination)
163        }
164
165        #[cfg(feature = "asyncapi")]
166        fn operation_bindings(&self, _destination: &str) -> Bindings {
167            crate::bindings::publish_operation(self.publish_options())
168        }
169
170        #[cfg(feature = "asyncapi")]
171        fn reply_address_location(&self) -> Option<&'static str> {
172            Some(crate::bindings::REPLY_ADDRESS_LOCATION)
173        }
174    };
175}
176
177pub(crate) use publish_policy_bindings;
178
179/// A publish policy that pairs with a connected `RabbitMQ` broker without opening a channel.
180///
181/// All of this crate's policies hold nothing but publish options, so bringing one alive is a
182/// constructor call rather than broker work. That is what lets
183/// [`ConnectedLapinBroker::publisher`] be synchronous; [`PublishPolicy::pair`], the
184/// framework-side entry point, delegates here.
185pub trait LapinPublishPolicy: PublishPolicy<ConnectedLapinBroker> + Sealed {
186    /// Pairs the policy with the connected broker, producing the live publisher.
187    #[must_use]
188    fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live;
189}
190
191/// The fire-and-forget publish policy: pure declaration, constructible anywhere.
192///
193/// [`OutgoingMessage::name`](ruststream::OutgoingMessage::name) is the routing key; the target
194/// exchange is a property of the policy (the default exchange unless
195/// [`exchange`](Self::exchange) says otherwise). On the default exchange the routing key
196/// addresses the queue with that name.
197///
198/// The policy is also where the per-message AMQP properties get their defaults:
199/// [`persistent`](Self::persistent), [`priority`](Self::priority) and
200/// [`expiration`](Self::expiration) fix what every publish through it carries, and the publish
201/// builder's own steps ([`LapinPublishSteps`](crate::LapinPublishSteps)) adjust one message over
202/// them.
203///
204/// It pairs into [`LapinPublisher`], and it is the broker's
205/// [`DefaultPublish`](ruststream::DefaultPublish) policy, so a `reply("dest")` handler mounted
206/// without an explicit `out_reply` policy replies through it. [`confirms`](Self::confirms) and
207/// [`server_tx`](Self::server_tx) move to the transactional policies, keeping the settings.
208///
209/// # Examples
210///
211/// ```
212/// use ruststream_lapin::prelude::*;
213/// use serde::{Deserialize, Serialize};
214///
215/// #[derive(Deserialize)]
216/// struct Order {
217///     id: u64,
218/// }
219///
220/// #[derive(Serialize, Outgoing)]
221/// #[outgoing(name = "order.placed")]
222/// struct OrderPlaced {
223///     id: u64,
224/// }
225///
226/// #[subscriber("orders")]
227/// async fn announce(order: &Order, Out(events): Out<impl Publisher>) -> HandlerOutcome {
228///     let placed = OrderPlaced { id: order.id };
229///     if events.message(&placed).publish().await.is_err() {
230///         return HandlerOutcome::retry();
231///     }
232///     HandlerOutcome::ack()
233/// }
234///
235/// // Every announcement goes to the `events` exchange under routing key `order.placed`.
236/// fn app() -> RustStream {
237///     let broker = LapinBroker::new("amqp://localhost:5672");
238///     let events = LapinPublish::default().exchange("events").priority(3);
239///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
240///         b.include(announce).out(DefaultSlot, events).build();
241///     })
242/// }
243/// ```
244#[derive(Debug, Clone, Default, PartialEq, Eq)]
245#[must_use]
246pub struct LapinPublish(PublishOptions);
247
248impl LapinPublish {
249    /// Moves to the policy that awaits broker confirms, with buffering transactions.
250    ///
251    /// The recommended transactional publisher: durable and much faster than AMQP server
252    /// transactions.
253    pub fn confirms(self) -> ConfirmsPublish {
254        ConfirmsPublish(self.0)
255    }
256
257    /// Moves to the policy backed by AMQP server transactions (`tx.select`).
258    ///
259    /// Server-side atomicity, at the cost of a synchronous commit round trip that is
260    /// significantly slower than [`confirms`](Self::confirms).
261    pub fn server_tx(self) -> ServerTxPublish {
262        ServerTxPublish(self.0)
263    }
264}
265
266publish_policy_settings!(LapinPublish);
267
268impl PublishPolicy<ConnectedLapinBroker> for LapinPublish {
269    type Live = LapinPublisher;
270
271    fn pair(
272        self,
273        connected: &ConnectedLapinBroker,
274    ) -> impl Future<Output = Result<Self::Live, PairError>> {
275        ready(Ok(self.bind(connected)))
276    }
277
278    publish_policy_bindings!();
279}
280
281impl LapinPublishPolicy for LapinPublish {
282    fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live {
283        LapinPublisher::new(connected, self.0)
284    }
285}
286
287/// The confirm-transactional publish policy: same options as [`LapinPublish`], pairing into
288/// [`ConfirmsPublisher`].
289///
290/// Reached with [`LapinPublish::confirms`].
291///
292/// # Examples
293///
294/// ```
295/// use ruststream_lapin::prelude::*;
296/// use serde::{Deserialize, Serialize};
297///
298/// #[derive(Deserialize)]
299/// struct Order {
300///     id: u64,
301/// }
302///
303/// #[derive(Serialize, Outgoing)]
304/// #[outgoing(name = "shipment.requested")]
305/// struct Shipment {
306///     order_id: u64,
307/// }
308///
309/// // The order is acknowledged only once the broker confirmed the shipment.
310/// #[subscriber("orders")]
311/// async fn ship(order: &Order, Out(shipments): Out<impl TransactionalPublisher>) -> HandlerOutcome {
312///     let shipment = Shipment { order_id: order.id };
313///     if shipments.message(&shipment).publish().await.is_err() {
314///         return HandlerOutcome::retry();
315///     }
316///     HandlerOutcome::ack()
317/// }
318///
319/// fn app() -> RustStream {
320///     let broker = LapinBroker::new("amqp://localhost:5672");
321///     let shipments = LapinPublish::default().exchange("shipments").confirms();
322///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
323///         b.include(ship).out(DefaultSlot, shipments).build();
324///     })
325/// }
326/// ```
327#[derive(Debug, Clone, Default, PartialEq, Eq)]
328#[must_use]
329pub struct ConfirmsPublish(PublishOptions);
330
331publish_policy_settings!(ConfirmsPublish);
332
333impl PublishPolicy<ConnectedLapinBroker> for ConfirmsPublish {
334    type Live = ConfirmsPublisher;
335
336    fn pair(
337        self,
338        connected: &ConnectedLapinBroker,
339    ) -> impl Future<Output = Result<Self::Live, PairError>> {
340        ready(Ok(self.bind(connected)))
341    }
342
343    publish_policy_bindings!();
344}
345
346impl LapinPublishPolicy for ConfirmsPublish {
347    fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live {
348        ConfirmsPublisher::new(connected, self.0)
349    }
350}
351
352/// The server-transactional publish policy: same options as [`LapinPublish`], pairing into
353/// [`ServerTxPublisher`].
354///
355/// Reached with [`LapinPublish::server_tx`].
356///
357/// # Examples
358///
359/// ```
360/// use std::error::Error;
361///
362/// use ruststream_lapin::prelude::*;
363/// use serde::{Deserialize, Serialize};
364///
365/// #[derive(Deserialize)]
366/// struct Transfer {
367///     from: u64,
368///     to: u64,
369///     cents: i64,
370/// }
371///
372/// #[derive(Serialize, Outgoing)]
373/// #[outgoing(name = "ledger.entry")]
374/// struct Entry {
375///     account: u64,
376///     cents: i64,
377/// }
378///
379/// // Both legs of a transfer become visible together, or neither does.
380/// async fn post(
381///     ledger: &impl TransactionalPublisher,
382///     transfer: &Transfer,
383/// ) -> Result<(), Box<dyn Error + Send + Sync>> {
384///     let debit = Entry {
385///         account: transfer.from,
386///         cents: -transfer.cents,
387///     };
388///     let credit = Entry {
389///         account: transfer.to,
390///         cents: transfer.cents,
391///     };
392///     let mut scope = ledger.begin().await?;
393///     for entry in [debit, credit] {
394///         if let Err(err) = scope.message(&entry).publish().await {
395///             scope.abort().await?;
396///             return Err(err.into());
397///         }
398///     }
399///     scope.commit().await?;
400///     Ok(())
401/// }
402///
403/// #[subscriber("transfers")]
404/// async fn book(
405///     transfer: &Transfer,
406///     Out(ledger): Out<impl TransactionalPublisher>,
407/// ) -> HandlerOutcome {
408///     if post(ledger, transfer).await.is_err() {
409///         return HandlerOutcome::retry();
410///     }
411///     HandlerOutcome::ack()
412/// }
413///
414/// fn app() -> RustStream {
415///     let broker = LapinBroker::new("amqp://localhost:5672");
416///     let ledger = LapinPublish::default().exchange("ledger").server_tx();
417///     RustStream::new(AppInfo::new("ledger", "0.1.0")).with_broker(broker, |b| {
418///         b.include(book).out(DefaultSlot, ledger).build();
419///     })
420/// }
421/// ```
422#[derive(Debug, Clone, Default, PartialEq, Eq)]
423#[must_use]
424pub struct ServerTxPublish(PublishOptions);
425
426publish_policy_settings!(ServerTxPublish);
427
428impl PublishPolicy<ConnectedLapinBroker> for ServerTxPublish {
429    type Live = ServerTxPublisher;
430
431    fn pair(
432        self,
433        connected: &ConnectedLapinBroker,
434    ) -> impl Future<Output = Result<Self::Live, PairError>> {
435        ready(Ok(self.bind(connected)))
436    }
437
438    publish_policy_bindings!();
439}
440
441impl LapinPublishPolicy for ServerTxPublish {
442    fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live {
443        ServerTxPublisher::new(connected, self.0)
444    }
445}