#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::fmt::{Debug, Formatter};
use std::sync::OnceLock;
use std::time::Duration;
use async_nats::jetstream::AckKind;
use ruststream::{AckError, HeaderMap, IncomingMessage, Partitioned, Str};
use crate::convert::headers_from_nats;
#[cfg(feature = "testing")]
use crate::in_process::{JetStreamDelivery, Release};
pub enum NatsMessage {
Core(Box<CoreMessage>),
JetStream(Box<JetStreamMessage>),
}
impl Debug for NatsMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Core(_) => f.debug_struct("NatsMessage::Core").finish_non_exhaustive(),
Self::JetStream(_) => f
.debug_struct("NatsMessage::JetStream")
.finish_non_exhaustive(),
}
}
}
pub struct CoreMessage {
inner: async_nats::Message,
headers: Option<HeaderMap>,
#[cfg(feature = "testing")]
_release: Release,
}
impl Debug for CoreMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CoreMessage")
.field("subject", &self.inner.subject.as_str())
.field("payload_len", &self.inner.payload.len())
.finish_non_exhaustive()
}
}
impl CoreMessage {
pub(crate) fn new(inner: async_nats::Message) -> Self {
if inner.headers.is_none() && inner.reply.is_none() {
return Self {
inner,
headers: None,
#[cfg(feature = "testing")]
_release: Release::uncounted(),
};
}
let mut headers = headers_from_nats(inner.headers.as_ref());
if let Some(reply) = inner.reply.as_ref() {
headers.insert(Str::from_static(REPLY_TO_HEADER), reply.as_str().to_owned());
}
Self {
inner,
headers: Some(headers),
#[cfg(feature = "testing")]
_release: Release::uncounted(),
}
}
#[cfg(feature = "testing")]
pub(crate) fn in_process(inner: async_nats::Message, release: Release) -> Self {
Self {
_release: release,
..Self::new(inner)
}
}
}
pub struct JetStreamMessage {
inner: JetStreamInner,
headers: Option<HeaderMap>,
}
#[cfg_attr(feature = "testing", allow(clippy::large_enum_variant))]
enum JetStreamInner {
Nats(async_nats::jetstream::Message),
#[cfg(feature = "testing")]
InProcess(Box<JetStreamDelivery>),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<JetStreamInner>() == size_of::<async_nats::jetstream::Message>());
pub(crate) struct DeliveryMetadata<'a> {
pub(crate) stream: &'a str,
pub(crate) consumer: &'a str,
pub(crate) stream_sequence: u64,
pub(crate) consumer_sequence: u64,
pub(crate) delivered: i64,
pub(crate) pending: u64,
}
impl Debug for JetStreamMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JetStreamMessage")
.field("subject", &self.message().subject.as_str())
.field("payload_len", &self.message().payload.len())
.finish_non_exhaustive()
}
}
impl JetStreamMessage {
pub(crate) fn new(inner: async_nats::jetstream::Message) -> Self {
let headers = inner
.message
.headers
.as_ref()
.map(|wire| headers_from_nats(Some(wire)));
Self {
inner: JetStreamInner::Nats(inner),
headers,
}
}
#[cfg(feature = "testing")]
pub(crate) fn in_process(delivery: JetStreamDelivery) -> Self {
let headers = delivery
.message
.headers
.as_ref()
.map(|wire| headers_from_nats(Some(wire)));
Self {
inner: JetStreamInner::InProcess(Box::new(delivery)),
headers,
}
}
fn message(&self) -> &async_nats::Message {
match &self.inner {
JetStreamInner::Nats(inner) => &inner.message,
#[cfg(feature = "testing")]
JetStreamInner::InProcess(delivery) => &delivery.message,
}
}
pub(crate) fn metadata(&self) -> Option<DeliveryMetadata<'_>> {
match &self.inner {
JetStreamInner::Nats(inner) => inner.info().ok().map(|info| DeliveryMetadata {
stream: info.stream,
consumer: info.consumer,
stream_sequence: info.stream_sequence,
consumer_sequence: info.consumer_sequence,
delivered: info.delivered,
pending: info.pending,
}),
#[cfg(feature = "testing")]
JetStreamInner::InProcess(delivery) => Some(DeliveryMetadata {
stream: &delivery.info.stream,
consumer: &delivery.info.consumer,
stream_sequence: delivery.info.stream_sequence,
consumer_sequence: delivery.info.consumer_sequence,
delivered: i64::try_from(delivery.info.delivered).unwrap_or(i64::MAX),
pending: delivery.info.pending,
}),
}
}
}
pub(crate) const REPLY_TO_HEADER: &str = "reply-to";
#[cfg(feature = "asyncapi")]
pub(crate) const REPLY_ADDRESS_LOCATION: &str = "$message.header#/reply-to";
fn empty_headers() -> &'static HeaderMap {
static EMPTY: OnceLock<HeaderMap> = OnceLock::new();
EMPTY.get_or_init(HeaderMap::new)
}
impl IncomingMessage for NatsMessage {
fn payload(&self) -> &[u8] {
match self {
Self::Core(m) => &m.inner.payload,
Self::JetStream(m) => &m.message().payload,
}
}
fn headers(&self) -> &HeaderMap {
let carried = match self {
Self::Core(m) => m.headers.as_ref(),
Self::JetStream(m) => m.headers.as_ref(),
};
carried.unwrap_or_else(|| empty_headers())
}
fn partition_key(&self) -> Option<&[u8]> {
self.headers().get(PARTITION_KEY_HEADER)
}
async fn ack(self) -> Result<(), AckError> {
match self {
Self::Core(_) => Err(AckError::Unsupported),
Self::JetStream(m) => match m.inner {
JetStreamInner::Nats(inner) => inner
.ack()
.await
.map_err(|err| AckError::Broker(format_err(err))),
#[cfg(feature = "testing")]
JetStreamInner::InProcess(delivery) => delivery.ack(),
},
}
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
match self {
Self::Core(_) => Err(AckError::Unsupported),
Self::JetStream(m) => {
let inner = match m.inner {
JetStreamInner::Nats(inner) => inner,
#[cfg(feature = "testing")]
JetStreamInner::InProcess(delivery) => return delivery.nack(requeue),
};
let kind = if requeue {
AckKind::Nak(None)
} else {
AckKind::Term
};
inner
.ack_with(kind)
.await
.map_err(|err| AckError::Broker(format_err(err)))
}
}
}
fn redelivery_count(&self) -> Option<u64> {
match self {
Self::Core(_) => None,
Self::JetStream(m) => m.metadata().map(|info| info.delivered.unsigned_abs()),
}
}
fn supports_nack_after(&self) -> bool {
matches!(self, Self::JetStream(_))
}
async fn nack_after(self, delay: Duration) -> Result<(), AckError> {
match self {
Self::Core(_) => Err(AckError::Unsupported),
Self::JetStream(m) => match m.inner {
JetStreamInner::Nats(inner) => inner
.ack_with(AckKind::Nak(Some(delay)))
.await
.map_err(|err| AckError::Broker(format_err(err))),
#[cfg(feature = "testing")]
JetStreamInner::InProcess(delivery) => delivery.nack_after(delay),
},
}
}
}
pub const PARTITION_KEY_HEADER: &str = "nats-partition-key";
impl Partitioned for NatsMessage {
fn partition_key(&self) -> Option<&[u8]> {
IncomingMessage::partition_key(self)
}
}
fn format_err<E>(err: E) -> Box<dyn std::error::Error + Send + Sync>
where
E: std::fmt::Display + Send + Sync + 'static,
{
let msg = err.to_string();
Box::<dyn std::error::Error + Send + Sync>::from(msg)
}
#[cfg(test)]
mod tests {
use super::*;
fn core_message(reply: Option<&str>) -> NatsMessage {
NatsMessage::Core(Box::new(CoreMessage::new(async_nats::Message {
subject: "subj".into(),
reply: reply.map(Into::into),
payload: bytes::Bytes::from_static(b"x"),
headers: None,
status: None,
description: None,
length: 1,
})))
}
#[test]
fn a_headerless_delivery_answers_with_the_shared_empty_map() {
let first = core_message(None);
let second = core_message(None);
assert!(
std::ptr::eq(first.headers(), second.headers()),
"a delivery with neither wire headers nor a reply inbox has nothing to build a map \
from, so both answer the crate's own empty one",
);
}
#[test]
fn core_reply_inbox_surfaces_as_reply_to_header() {
let msg = core_message(Some("_INBOX.42"));
assert_eq!(msg.headers().reply_to(), Some("_INBOX.42"));
}
#[test]
fn core_message_without_reply_has_no_reply_to() {
assert_eq!(core_message(None).headers().reply_to(), None);
}
#[test]
fn core_delivery_does_not_claim_native_delayed_redelivery() {
assert!(!core_message(None).supports_nack_after());
}
#[tokio::test]
async fn core_delivery_reports_nack_after_unsupported() {
let err = core_message(None)
.nack_after(Duration::from_secs(1))
.await
.expect_err("core NATS cannot honor a delayed redelivery");
assert!(matches!(err, AckError::Unsupported));
}
}