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}