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#[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}