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#[derive(Clone)]
132pub struct NatsPublisher {
133 link: Link,
134}
135
136// A production build holds the connection handle and nothing else.
137#[cfg(not(feature = "testing"))]
138const _: () = assert!(size_of::<NatsPublisher>() == size_of::<Arc<NatsConnection>>());
139
140impl Debug for NatsPublisher {
141 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
142 f.debug_struct("NatsPublisher").finish_non_exhaustive()
143 }
144}
145
146impl NatsPublisher {
147 pub(crate) const fn new(link: Link) -> Self {
148 Self { link }
149 }
150
151 /// What this publisher speaks over.
152 pub(crate) const fn link(&self) -> &Link {
153 &self.link
154 }
155}
156
157/// The live client, lent rather than handed over: `Client::publish` takes `&self` and the borrow
158/// lives as long as the publish future, so a publish has no reason to own a copy of the
159/// connection.
160pub(crate) fn client_for<'a>(
161 connection: &'a NatsConnection,
162 subject: &str,
163) -> Result<&'a Client, NatsError> {
164 connection.live_client(subject)
165}
166
167impl Publisher for NatsPublisher {
168 /// The client keeps the payload: `async-nats` publishes a `Bytes`, so the buffer the
169 /// framework wrote is handed over instead of being read and copied.
170 type Payload = Take;
171
172 type Error = NatsError;
173
174 /// Core NATS says nothing about a message beyond its subject, its payload and its headers, so
175 /// there is no per-message setting to carry. `JetStream` has four, and they live on
176 /// [`JetStreamOptions`](crate::JetStreamOptions).
177 type Options = ();
178
179 /// # Cancel safety
180 ///
181 /// Core NATS publishing is fire-and-forget: the message is handed to the connection's writer
182 /// without waiting for the server. Dropping the future may leave the message either sent or
183 /// unsent, with no way to tell which.
184 async fn publish(
185 &self,
186 msg: OutgoingMessage<'_, BytesMut>,
187 _options: Option<&Self::Options>,
188 ) -> Result<(), Self::Error> {
189 let connection = match &self.link {
190 Link::Nats(connection) => connection,
191 #[cfg(feature = "testing")]
192 Link::InProcess(bus) => {
193 let (subject, payload, headers) = msg.into_parts();
194 return bus
195 .publish(
196 subject,
197 payload.freeze(),
198 &headers,
199 None,
200 PublishMode::Core,
201 Origin::Connection,
202 )
203 .map(drop);
204 }
205 };
206 let client = client_for(connection, msg.name())?;
207 let (subject, payload, headers) = nats_parts(msg)?;
208 let result = match headers {
209 Some(headers) => client.publish_with_headers(subject, headers, payload).await,
210 None => client.publish(subject, payload).await,
211 };
212 result.map_err(|err| NatsError::Publish(Box::new(err)))
213 }
214}