#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::fmt::{Debug, Formatter};
use std::future::{Future, ready};
use std::sync::Arc;
use async_nats::jetstream::Context;
use async_nats::jetstream::message::PublishMessage;
use bytes::BytesMut;
#[cfg(feature = "testing")]
use ruststream::HeaderMap;
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::{Binding, Bindings};
use ruststream::runtime::{PublishBuilder, PublishSink};
use ruststream::{OutgoingMessage, PairError, PublishPolicy, Publisher, Take};
#[cfg(feature = "asyncapi")]
use serde::Serialize;
use crate::broker::{ConnectedNatsBroker, Link, NatsConnection};
#[cfg(feature = "testing")]
use crate::in_process::{Bus, Origin, PublishMode};
use crate::publisher::NatsPublishPolicy;
#[cfg(feature = "asyncapi")]
use crate::subject::JETSTREAM_EXTENSION;
use crate::{convert::nats_parts, error::NatsError};
pub use async_nats::jetstream::publish::PublishAck;
#[cfg(feature = "testing")]
pub(crate) const MESSAGE_ID: &str = "Nats-Msg-Id";
#[cfg(feature = "testing")]
pub(crate) const EXPECTED_LAST_SEQUENCE: &str = "Nats-Expected-Last-Sequence";
#[cfg(feature = "testing")]
pub(crate) const EXPECTED_LAST_SUBJECT_SEQUENCE: &str = "Nats-Expected-Last-Subject-Sequence";
#[cfg(feature = "testing")]
pub(crate) const EXPECTED_LAST_MESSAGE_ID: &str = "Nats-Expected-Last-Msg-Id";
#[cfg(feature = "testing")]
pub(crate) const EXPECTED_STREAM: &str = "Nats-Expected-Stream";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct JetStreamOptions {
pub message_id: Option<String>,
pub expect_last_sequence: Option<u64>,
pub expect_last_subject_sequence: Option<u64>,
pub expect_last_message_id: Option<String>,
}
impl JetStreamOptions {
#[cfg(feature = "testing")]
pub(crate) fn write_headers(&self, headers: &mut HeaderMap) {
if let Some(id) = &self.message_id {
headers.insert(MESSAGE_ID, id.clone());
}
if let Some(sequence) = self.expect_last_sequence {
headers.insert(EXPECTED_LAST_SEQUENCE, sequence.to_string());
}
if let Some(sequence) = self.expect_last_subject_sequence {
headers.insert(EXPECTED_LAST_SUBJECT_SEQUENCE, sequence.to_string());
}
if let Some(id) = &self.expect_last_message_id {
headers.insert(EXPECTED_LAST_MESSAGE_ID, id.clone());
}
}
pub(crate) fn apply(&self, mut message: PublishMessage) -> PublishMessage {
if let Some(id) = &self.message_id {
message = message.message_id(id);
}
if let Some(sequence) = self.expect_last_sequence {
message = message.expected_last_sequence(sequence);
}
if let Some(sequence) = self.expect_last_subject_sequence {
message = message.expected_last_subject_sequence(sequence);
}
if let Some(id) = &self.expect_last_message_id {
message = message.expected_last_message_id(id);
}
message
}
}
pub trait JetStreamPublishSteps {
#[must_use]
fn message_id(self, id: impl Into<String>) -> Self;
#[must_use]
fn expect_last_sequence(self, sequence: u64) -> Self;
#[must_use]
fn expect_last_subject_sequence(self, sequence: u64) -> Self;
#[must_use]
fn expect_last_message_id(self, id: impl Into<String>) -> Self;
}
impl<Sink, Body, Enc, Hdrs, Dest> JetStreamPublishSteps
for PublishBuilder<Sink, Body, Enc, Hdrs, Dest>
where
Sink: PublishSink<Options = JetStreamOptions>,
{
fn message_id(mut self, id: impl Into<String>) -> Self {
self.options_mut()
.get_or_insert_with(JetStreamOptions::default)
.message_id = Some(id.into());
self
}
fn expect_last_sequence(mut self, sequence: u64) -> Self {
self.options_mut()
.get_or_insert_with(JetStreamOptions::default)
.expect_last_sequence = Some(sequence);
self
}
fn expect_last_subject_sequence(mut self, sequence: u64) -> Self {
self.options_mut()
.get_or_insert_with(JetStreamOptions::default)
.expect_last_subject_sequence = Some(sequence);
self
}
fn expect_last_message_id(mut self, id: impl Into<String>) -> Self {
self.options_mut()
.get_or_insert_with(JetStreamOptions::default)
.expect_last_message_id = Some(id.into());
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[must_use]
pub struct JetStreamPublish {
stream: Option<String>,
}
impl JetStreamPublish {
pub fn expect_stream(mut self, stream: impl Into<String>) -> Self {
self.stream = Some(stream.into());
self
}
fn apply(&self, message: PublishMessage) -> PublishMessage {
match &self.stream {
Some(stream) => message.expected_stream(stream),
None => message,
}
}
#[cfg(feature = "testing")]
pub(crate) fn write_headers(&self, headers: &mut HeaderMap) {
if let Some(stream) = &self.stream {
headers.insert(EXPECTED_STREAM, stream.clone());
}
}
}
impl PublishPolicy<ConnectedNatsBroker> for JetStreamPublish {
type Live = JetStreamPublisher;
fn pair(
self,
connected: &ConnectedNatsBroker,
) -> impl Future<Output = Result<Self::Live, PairError>> {
ready(Ok(self.bind(connected)))
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self, channel: &str) -> Bindings {
self.stream
.as_deref()
.map(|stream| JetStreamPublishChannel {
expected_stream: stream,
subject: channel,
})
.and_then(|body| Binding::extension(JETSTREAM_EXTENSION, &body).ok())
.map(|binding| Bindings::new().with(binding))
.unwrap_or_default()
}
}
#[cfg(feature = "asyncapi")]
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct JetStreamPublishChannel<'a> {
expected_stream: &'a str,
subject: &'a str,
}
impl NatsPublishPolicy for JetStreamPublish {
fn bind(self, connected: &ConnectedNatsBroker) -> Self::Live {
let link = match connected.link() {
Link::Nats(connection) => JetStreamLink::Nats {
connection: Arc::clone(connection),
context: connection.jetstream(),
},
#[cfg(feature = "testing")]
Link::InProcess(bus) => JetStreamLink::InProcess(Arc::clone(bus)),
};
JetStreamPublisher { link, policy: self }
}
}
#[cfg_attr(feature = "testing", allow(clippy::large_enum_variant))]
#[derive(Clone)]
enum JetStreamLink {
Nats {
connection: Arc<NatsConnection>,
context: Context,
},
#[cfg(feature = "testing")]
InProcess(Arc<Bus>),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<JetStreamLink>() == size_of::<(Arc<NatsConnection>, Context)>());
#[derive(Clone)]
pub struct JetStreamPublisher {
link: JetStreamLink,
policy: JetStreamPublish,
}
impl Debug for JetStreamPublisher {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JetStreamPublisher")
.field("policy", &self.policy)
.finish_non_exhaustive()
}
}
impl JetStreamPublisher {
pub async fn publish_ack(
&self,
msg: OutgoingMessage<'_, BytesMut>,
options: Option<&JetStreamOptions>,
) -> Result<PublishAck, NatsError> {
let (connection, context) = match &self.link {
JetStreamLink::Nats {
connection,
context,
} => (connection, context),
#[cfg(feature = "testing")]
JetStreamLink::InProcess(bus) => return self.publish_in_process(bus, msg, options),
};
connection.live_client(msg.name())?;
let (subject, payload, headers) = nats_parts(msg)?;
let mut message = PublishMessage::build().payload(payload);
if let Some(headers) = headers {
message = message.headers(headers);
}
message = self.policy.apply(message);
if let Some(options) = options {
message = options.apply(message);
}
context
.send_publish(subject, message)
.await
.map_err(|err| NatsError::Publish(Box::new(err)))?
.await
.map_err(|err| NatsError::JetStream(Box::new(err)))
}
}
#[cfg(feature = "testing")]
impl JetStreamPublisher {
fn publish_in_process(
&self,
bus: &Bus,
msg: OutgoingMessage<'_, BytesMut>,
options: Option<&JetStreamOptions>,
) -> Result<PublishAck, NatsError> {
let (subject, payload, mut headers) = msg.into_parts();
self.policy.write_headers(&mut headers);
if let Some(options) = options {
options.write_headers(&mut headers);
}
let stored = bus
.publish(
subject,
payload.freeze(),
&headers,
None,
PublishMode::JetStream,
Origin::Connection,
)?
.ok_or_else(|| NatsError::JetStream("no stream found for given subject".into()))?;
Ok(PublishAck {
stream: stored.stream,
sequence: stored.sequence,
duplicate: stored.duplicate,
..PublishAck::default()
})
}
}
impl Publisher for JetStreamPublisher {
type Payload = Take;
type Error = NatsError;
type Options = JetStreamOptions;
async fn publish(
&self,
msg: OutgoingMessage<'_, BytesMut>,
options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
self.publish_ack(msg, options).await.map(|_ack| ())
}
}