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