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}