1#![cfg_attr(
5 not(feature = "testing"),
6 allow(clippy::infallible_destructuring_match)
7)]
8
9use std::fmt::{Debug, Formatter};
10use std::sync::OnceLock;
11use std::time::Duration;
12
13use async_nats::jetstream::AckKind;
14use ruststream::{AckError, HeaderMap, IncomingMessage, Partitioned, Str};
15
16use crate::convert::headers_from_nats;
17#[cfg(feature = "testing")]
18use crate::in_process::{JetStreamDelivery, Release};
19
20pub enum NatsMessage {
24 Core(Box<CoreMessage>),
26 JetStream(Box<JetStreamMessage>),
28}
29
30impl Debug for NatsMessage {
31 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Self::Core(_) => f.debug_struct("NatsMessage::Core").finish_non_exhaustive(),
34 Self::JetStream(_) => f
35 .debug_struct("NatsMessage::JetStream")
36 .finish_non_exhaustive(),
37 }
38 }
39}
40
41pub struct CoreMessage {
43 inner: async_nats::Message,
44 headers: Option<HeaderMap>,
48 #[cfg(feature = "testing")]
51 _release: Release,
52}
53
54impl Debug for CoreMessage {
55 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56 f.debug_struct("CoreMessage")
57 .field("subject", &self.inner.subject.as_str())
58 .field("payload_len", &self.inner.payload.len())
59 .finish_non_exhaustive()
60 }
61}
62
63impl CoreMessage {
64 pub(crate) fn new(inner: async_nats::Message) -> Self {
65 if inner.headers.is_none() && inner.reply.is_none() {
66 return Self {
67 inner,
68 headers: None,
69 #[cfg(feature = "testing")]
70 _release: Release::uncounted(),
71 };
72 }
73 let mut headers = headers_from_nats(inner.headers.as_ref());
74 if let Some(reply) = inner.reply.as_ref() {
80 headers.insert(Str::from_static(REPLY_TO_HEADER), reply.as_str().to_owned());
81 }
82 Self {
83 inner,
84 headers: Some(headers),
85 #[cfg(feature = "testing")]
86 _release: Release::uncounted(),
87 }
88 }
89
90 #[cfg(feature = "testing")]
93 pub(crate) fn in_process(inner: async_nats::Message, release: Release) -> Self {
94 Self {
95 _release: release,
96 ..Self::new(inner)
97 }
98 }
99}
100
101pub struct JetStreamMessage {
103 inner: JetStreamInner,
104 headers: Option<HeaderMap>,
106}
107
108#[cfg_attr(feature = "testing", allow(clippy::large_enum_variant))]
113enum JetStreamInner {
114 Nats(async_nats::jetstream::Message),
115 #[cfg(feature = "testing")]
116 InProcess(Box<JetStreamDelivery>),
117}
118
119#[cfg(not(feature = "testing"))]
121const _: () = assert!(size_of::<JetStreamInner>() == size_of::<async_nats::jetstream::Message>());
122
123pub(crate) struct DeliveryMetadata<'a> {
125 pub(crate) stream: &'a str,
126 pub(crate) consumer: &'a str,
127 pub(crate) stream_sequence: u64,
128 pub(crate) consumer_sequence: u64,
129 pub(crate) delivered: i64,
130 pub(crate) pending: u64,
131}
132
133impl Debug for JetStreamMessage {
134 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
135 f.debug_struct("JetStreamMessage")
136 .field("subject", &self.message().subject.as_str())
137 .field("payload_len", &self.message().payload.len())
138 .finish_non_exhaustive()
139 }
140}
141
142impl JetStreamMessage {
143 pub(crate) fn new(inner: async_nats::jetstream::Message) -> Self {
144 let headers = inner
145 .message
146 .headers
147 .as_ref()
148 .map(|wire| headers_from_nats(Some(wire)));
149 Self {
150 inner: JetStreamInner::Nats(inner),
151 headers,
152 }
153 }
154
155 #[cfg(feature = "testing")]
157 pub(crate) fn in_process(delivery: JetStreamDelivery) -> Self {
158 let headers = delivery
159 .message
160 .headers
161 .as_ref()
162 .map(|wire| headers_from_nats(Some(wire)));
163 Self {
164 inner: JetStreamInner::InProcess(Box::new(delivery)),
165 headers,
166 }
167 }
168
169 fn message(&self) -> &async_nats::Message {
171 match &self.inner {
172 JetStreamInner::Nats(inner) => &inner.message,
173 #[cfg(feature = "testing")]
174 JetStreamInner::InProcess(delivery) => &delivery.message,
175 }
176 }
177
178 pub(crate) fn metadata(&self) -> Option<DeliveryMetadata<'_>> {
185 match &self.inner {
186 JetStreamInner::Nats(inner) => inner.info().ok().map(|info| DeliveryMetadata {
187 stream: info.stream,
188 consumer: info.consumer,
189 stream_sequence: info.stream_sequence,
190 consumer_sequence: info.consumer_sequence,
191 delivered: info.delivered,
192 pending: info.pending,
193 }),
194 #[cfg(feature = "testing")]
195 JetStreamInner::InProcess(delivery) => Some(DeliveryMetadata {
196 stream: &delivery.info.stream,
197 consumer: &delivery.info.consumer,
198 stream_sequence: delivery.info.stream_sequence,
199 consumer_sequence: delivery.info.consumer_sequence,
200 delivered: i64::try_from(delivery.info.delivered).unwrap_or(i64::MAX),
201 pending: delivery.info.pending,
202 }),
203 }
204 }
205}
206
207pub(crate) const REPLY_TO_HEADER: &str = "reply-to";
215
216#[cfg(feature = "asyncapi")]
220pub(crate) const REPLY_ADDRESS_LOCATION: &str = "$message.header#/reply-to";
221
222fn empty_headers() -> &'static HeaderMap {
227 static EMPTY: OnceLock<HeaderMap> = OnceLock::new();
228 EMPTY.get_or_init(HeaderMap::new)
229}
230
231impl IncomingMessage for NatsMessage {
232 fn payload(&self) -> &[u8] {
233 match self {
234 Self::Core(m) => &m.inner.payload,
235 Self::JetStream(m) => &m.message().payload,
236 }
237 }
238
239 fn headers(&self) -> &HeaderMap {
240 let carried = match self {
241 Self::Core(m) => m.headers.as_ref(),
242 Self::JetStream(m) => m.headers.as_ref(),
243 };
244 carried.unwrap_or_else(|| empty_headers())
245 }
246
247 fn partition_key(&self) -> Option<&[u8]> {
250 self.headers().get(PARTITION_KEY_HEADER)
251 }
252
253 async fn ack(self) -> Result<(), AckError> {
254 match self {
255 Self::Core(_) => Err(AckError::Unsupported),
256 Self::JetStream(m) => match m.inner {
257 JetStreamInner::Nats(inner) => inner
258 .ack()
259 .await
260 .map_err(|err| AckError::Broker(format_err(err))),
261 #[cfg(feature = "testing")]
262 JetStreamInner::InProcess(delivery) => delivery.ack(),
263 },
264 }
265 }
266
267 async fn nack(self, requeue: bool) -> Result<(), AckError> {
268 match self {
269 Self::Core(_) => Err(AckError::Unsupported),
270 Self::JetStream(m) => {
271 let inner = match m.inner {
272 JetStreamInner::Nats(inner) => inner,
273 #[cfg(feature = "testing")]
274 JetStreamInner::InProcess(delivery) => return delivery.nack(requeue),
275 };
276 let kind = if requeue {
277 AckKind::Nak(None)
278 } else {
279 AckKind::Term
280 };
281 inner
282 .ack_with(kind)
283 .await
284 .map_err(|err| AckError::Broker(format_err(err)))
285 }
286 }
287 }
288
289 fn redelivery_count(&self) -> Option<u64> {
297 match self {
298 Self::Core(_) => None,
299 Self::JetStream(m) => m.metadata().map(|info| info.delivered.unsigned_abs()),
300 }
301 }
302
303 fn supports_nack_after(&self) -> bool {
310 matches!(self, Self::JetStream(_))
311 }
312
313 async fn nack_after(self, delay: Duration) -> Result<(), AckError> {
324 match self {
325 Self::Core(_) => Err(AckError::Unsupported),
326 Self::JetStream(m) => match m.inner {
327 JetStreamInner::Nats(inner) => inner
328 .ack_with(AckKind::Nak(Some(delay)))
329 .await
330 .map_err(|err| AckError::Broker(format_err(err))),
331 #[cfg(feature = "testing")]
332 JetStreamInner::InProcess(delivery) => delivery.nack_after(delay),
333 },
334 }
335 }
336}
337
338pub const PARTITION_KEY_HEADER: &str = "nats-partition-key";
346
347impl Partitioned for NatsMessage {
351 fn partition_key(&self) -> Option<&[u8]> {
352 IncomingMessage::partition_key(self)
353 }
354}
355
356fn format_err<E>(err: E) -> Box<dyn std::error::Error + Send + Sync>
357where
358 E: std::fmt::Display + Send + Sync + 'static,
359{
360 let msg = err.to_string();
361 Box::<dyn std::error::Error + Send + Sync>::from(msg)
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 fn core_message(reply: Option<&str>) -> NatsMessage {
369 NatsMessage::Core(Box::new(CoreMessage::new(async_nats::Message {
370 subject: "subj".into(),
371 reply: reply.map(Into::into),
372 payload: bytes::Bytes::from_static(b"x"),
373 headers: None,
374 status: None,
375 description: None,
376 length: 1,
377 })))
378 }
379
380 #[test]
384 fn a_headerless_delivery_answers_with_the_shared_empty_map() {
385 let first = core_message(None);
386 let second = core_message(None);
387
388 assert!(
389 std::ptr::eq(first.headers(), second.headers()),
390 "a delivery with neither wire headers nor a reply inbox has nothing to build a map \
391 from, so both answer the crate's own empty one",
392 );
393 }
394
395 #[test]
396 fn core_reply_inbox_surfaces_as_reply_to_header() {
397 let msg = core_message(Some("_INBOX.42"));
398 assert_eq!(msg.headers().reply_to(), Some("_INBOX.42"));
399 }
400
401 #[test]
402 fn core_message_without_reply_has_no_reply_to() {
403 assert_eq!(core_message(None).headers().reply_to(), None);
404 }
405
406 #[test]
410 fn core_delivery_does_not_claim_native_delayed_redelivery() {
411 assert!(!core_message(None).supports_nack_after());
412 }
413
414 #[tokio::test]
415 async fn core_delivery_reports_nack_after_unsupported() {
416 let err = core_message(None)
417 .nack_after(Duration::from_secs(1))
418 .await
419 .expect_err("core NATS cannot honor a delayed redelivery");
420 assert!(matches!(err, AckError::Unsupported));
421 }
422}