Skip to main content

ruststream_nats/
publisher.rs

1//! Core NATS publishing: the [`NatsPublish`] policy and its live [`NatsPublisher`].
2
3// Without the `testing` feature a connection link has one variant, so a `match` on it has a
4// single arm; the matches stay so that the in-process arm has its place when the feature is on.
5#![cfg_attr(
6    not(feature = "testing"),
7    allow(clippy::infallible_destructuring_match)
8)]
9
10use std::fmt::{Debug, Formatter};
11use std::future::{Future, ready};
12#[cfg(not(feature = "testing"))]
13use std::sync::Arc;
14
15use async_nats::Client;
16use bytes::BytesMut;
17use ruststream::{OutgoingMessage, PairError, PublishPolicy, Publisher, Take};
18
19use crate::broker::{ConnectedNatsBroker, Link, NatsConnection};
20#[cfg(feature = "testing")]
21use crate::in_process::{Origin, PublishMode};
22#[cfg(feature = "asyncapi")]
23use crate::message::REPLY_ADDRESS_LOCATION;
24use crate::{convert::nats_parts, error::NatsError};
25
26use self::sealed::Sealed;
27
28mod sealed {
29    /// Seals [`NatsPublishPolicy`](super::NatsPublishPolicy): pairing a NATS publisher is
30    /// synchronous and infallible for both of this crate's policies, and the synchronous
31    /// [`publisher`](crate::ConnectedNatsBroker::publisher) accessor depends on that.
32    pub trait Sealed {}
33
34    impl Sealed for super::NatsPublish {}
35    impl Sealed for crate::jetstream::JetStreamPublish {}
36}
37
38/// A publish policy that pairs with a connected NATS broker without I/O.
39///
40/// Both NATS policies hold nothing but publish options, so bringing one alive is a constructor
41/// call rather than broker work. That is what lets
42/// [`ConnectedNatsBroker::publisher`](crate::ConnectedNatsBroker::publisher) be synchronous;
43/// [`PublishPolicy::pair`], the framework-side entry point, delegates here.
44pub trait NatsPublishPolicy: PublishPolicy<ConnectedNatsBroker> + Sealed {
45    /// Pairs the policy with the connected broker, producing the live publisher.
46    #[must_use]
47    fn bind(self, connected: &ConnectedNatsBroker) -> Self::Live;
48}
49
50/// The Core NATS publish policy: pure declaration, constructible anywhere.
51///
52/// Core NATS publishing carries no per-publisher options (subject and headers travel with each
53/// message), so the policy is a unit marker. It pairs into [`NatsPublisher`], which also serves
54/// the [`RequestReply`](ruststream::RequestReply) capability, and it is the broker's
55/// [`DefaultPublish`](ruststream::DefaultPublish) policy, so a replying handler mounted without
56/// an explicit publisher replies through it.
57///
58/// # Examples
59///
60/// ```
61/// # mod demo {
62/// use ruststream_nats::prelude::*;
63/// use serde::{Deserialize, Serialize};
64///
65/// #[derive(Deserialize)]
66/// struct Order {
67///     id: u64,
68/// }
69///
70/// #[derive(Serialize, Outgoing)]
71/// #[outgoing(name = "orders.seen")]
72/// struct Seen {
73///     id: u64,
74/// }
75///
76/// #[subscriber("orders.created", reply)]
77/// async fn acknowledge(order: &Order) -> Seen {
78///     Seen { id: order.id }
79/// }
80///
81/// #[ruststream::app]
82/// fn app() -> impl App {
83///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
84///         NatsBroker::new("nats://localhost:4222"),
85///         |b| {
86///             // `Publish` is this policy under the prelude's mount-site name.
87///             b.include(acknowledge).out_reply(Publish);
88///         },
89///     )
90/// }
91/// # }
92/// # fn main() {}
93/// ```
94#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
95#[must_use]
96pub struct NatsPublish;
97
98impl PublishPolicy<ConnectedNatsBroker> for NatsPublish {
99    type Live = NatsPublisher;
100
101    fn pair(
102        self,
103        connected: &ConnectedNatsBroker,
104    ) -> impl Future<Output = Result<Self::Live, PairError>> {
105        ready(Ok(self.bind(connected)))
106    }
107
108    /// Where a client reads the subject an answer goes to.
109    ///
110    /// A NATS request carries its inbox in a protocol field, which this crate surfaces as the
111    /// `reply-to` header, so that is the runtime expression the document reports for a reply this
112    /// policy publishes. The `nats` binding has nothing else for a publisher: its one field is a
113    /// queue group, which belongs to a subscription.
114    #[cfg(feature = "asyncapi")]
115    fn reply_address_location(&self) -> Option<&'static str> {
116        Some(REPLY_ADDRESS_LOCATION)
117    }
118}
119
120impl NatsPublishPolicy for NatsPublish {
121    fn bind(self, connected: &ConnectedNatsBroker) -> Self::Live {
122        NatsPublisher::new(connected.link().clone())
123    }
124}
125
126/// The live Core NATS publisher. Cheap to clone.
127///
128/// Exists only from a [`ConnectedNatsBroker`], so it always has a connection. It aliases that
129/// connection, though, and may outlive it: after the broker shuts down every publish reports
130/// [`NatsError::Closed`] instead of silently succeeding against a dead connection.
131///
132/// NATS correlates a reply with its request natively, so this publisher implements
133/// [`RequestReply`](ruststream::RequestReply): `request(msg, timeout)` publishes with a reply
134/// inbox and resolves with the reply, or with [`NatsError::RequestTimeout`] when nothing answers
135/// in time.
136///
137/// # Examples
138///
139/// ```
140/// # mod demo {
141/// use std::io;
142/// use std::time::Duration;
143///
144/// use ruststream::OutgoingMessage;
145/// use ruststream_nats::prelude::*;
146///
147/// #[ruststream::app]
148/// fn app() -> impl App {
149///     RustStream::new(AppInfo::new("requester", "0.1.0")).with_broker(
150///         NatsBroker::new("nats://localhost:4222"),
151///         |b| {
152///             b.after_startup(Publish, async move |requester| -> io::Result<()> {
153///                 let reply = requester
154///                     .request(
155///                         OutgoingMessage::new("questions", b"what is the answer?"),
156///                         Duration::from_secs(2),
157///                     )
158///                     .await
159///                     .map_err(io::Error::other)?;
160///                 tracing::info!(answer = %String::from_utf8_lossy(reply.payload()), "replied");
161///                 Ok(())
162///             });
163///         },
164///     )
165/// }
166/// # }
167/// # fn main() {}
168/// ```
169#[derive(Clone)]
170pub struct NatsPublisher {
171    link: Link,
172}
173
174// A production build holds the connection handle and nothing else.
175#[cfg(not(feature = "testing"))]
176const _: () = assert!(size_of::<NatsPublisher>() == size_of::<Arc<NatsConnection>>());
177
178impl Debug for NatsPublisher {
179    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
180        f.debug_struct("NatsPublisher").finish_non_exhaustive()
181    }
182}
183
184impl NatsPublisher {
185    pub(crate) const fn new(link: Link) -> Self {
186        Self { link }
187    }
188
189    /// What this publisher speaks over.
190    pub(crate) const fn link(&self) -> &Link {
191        &self.link
192    }
193}
194
195/// The live client, lent rather than handed over: `Client::publish` takes `&self` and the borrow
196/// lives as long as the publish future, so a publish has no reason to own a copy of the
197/// connection.
198pub(crate) fn client_for<'a>(
199    connection: &'a NatsConnection,
200    subject: &str,
201) -> Result<&'a Client, NatsError> {
202    connection.live_client(subject)
203}
204
205impl Publisher for NatsPublisher {
206    /// The client keeps the payload: `async-nats` publishes a `Bytes`, so the buffer the
207    /// framework wrote is handed over instead of being read and copied.
208    type Payload = Take;
209
210    type Error = NatsError;
211
212    /// Core NATS says nothing about a message beyond its subject, its payload and its headers, so
213    /// there is no per-message setting to carry. `JetStream` has four, and they live on
214    /// [`JetStreamOptions`](crate::JetStreamOptions).
215    type Options = ();
216
217    /// # Cancel safety
218    ///
219    /// Core NATS publishing is fire-and-forget: the message is handed to the connection's writer
220    /// without waiting for the server. Dropping the future may leave the message either sent or
221    /// unsent, with no way to tell which.
222    async fn publish(
223        &self,
224        msg: OutgoingMessage<'_, BytesMut>,
225        _options: Option<&Self::Options>,
226    ) -> Result<(), Self::Error> {
227        let connection = match &self.link {
228            Link::Nats(connection) => connection,
229            #[cfg(feature = "testing")]
230            Link::InProcess(bus) => {
231                let (subject, payload, headers) = msg.into_parts();
232                return bus
233                    .publish(
234                        subject,
235                        payload.freeze(),
236                        &headers,
237                        None,
238                        PublishMode::Core,
239                        Origin::Connection,
240                    )
241                    .map(drop);
242            }
243        };
244        let client = client_for(connection, msg.name())?;
245        let (subject, payload, headers) = nats_parts(msg)?;
246        let result = match headers {
247            Some(headers) => client.publish_with_headers(subject, headers, payload).await,
248            None => client.publish(subject, payload).await,
249        };
250        result.map_err(|err| NatsError::Publish(Box::new(err)))
251    }
252}