use std::future::{Future, ready};
use std::num::NonZeroU64;
use std::time::Duration;
pub use async_nats::jetstream::consumer::DeliverPolicy;
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::{Binding, Bindings};
use ruststream::runtime::IntoSource;
use ruststream::{
AddressedCopies, NamedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource,
};
#[cfg(feature = "asyncapi")]
use serde::Serialize;
use self::sealed::Sealed;
use crate::{ConnectedNatsBroker, error::NatsError, subscriber::NatsSubscriber};
const DEFAULT_ACK_WAIT: Duration = Duration::from_secs(30);
const DEFAULT_MAX_ACK_PENDING: i64 = 1024;
const DEFAULT_PULL_EXPIRES: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[must_use]
pub struct NonZeroDuration(Duration);
impl NonZeroDuration {
#[must_use]
pub const fn new(value: Duration) -> Option<Self> {
if value.is_zero() {
None
} else {
Some(Self(value))
}
}
pub const fn from_secs(secs: NonZeroU64) -> Self {
Self(Duration::from_secs(secs.get()))
}
pub const fn from_millis(millis: NonZeroU64) -> Self {
Self(Duration::from_millis(millis.get()))
}
#[must_use]
pub const fn get(self) -> Duration {
self.0
}
}
impl From<NonZeroDuration> for Duration {
fn from(value: NonZeroDuration) -> Self {
value.get()
}
}
#[doc(hidden)]
#[derive(Debug)]
pub enum SubscriptionPlan<'a> {
Core {
queue_group: Option<&'a str>,
},
JetStream {
stream: &'a str,
durable: Option<&'a str>,
filter_subject: &'a str,
ack_wait: Duration,
max_ack_pending: i64,
deliver_policy: DeliverPolicy,
pull_expires: Duration,
},
}
mod sealed {
use super::SubscriptionPlan;
pub trait Sealed {
fn plan(&self) -> SubscriptionPlan<'_>;
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` does not describe a NATS subscription",
label = "not a subscription descriptor",
note = "use `CoreSubject::new(subject)` for one Core NATS subject, \
`CoreWildcard::new(pattern)` for a Core NATS pattern, or \
`JetStreamSubject::new(subject, stream)` for a JetStream pull consumer"
)]
pub trait NatsSubscription: Sealed {
fn subject(&self) -> &str;
fn ensure_subject(&self) -> Result<(), NatsError> {
if self.subject().is_empty() {
return Err(NatsError::InvalidOptions(
"subject must be non-empty".into(),
));
}
Ok(())
}
}
fn has_wildcard(subject: &str) -> bool {
subject.split('.').any(|token| token == "*" || token == ">")
}
pub(crate) fn publish_destination(subject: &str) -> Result<RedeliveryAddress, NatsError> {
if subject.is_empty() {
return Err(NatsError::InvalidOptions(
"subject must be non-empty".into(),
));
}
if has_wildcard(subject) {
return Err(NatsError::InvalidOptions(format!(
"`{subject}` is a subscription pattern, and a NATS server refuses a publish to `*` \
or `>`. Subscribe to a pattern with `CoreWildcard`, which leaves the mount site to \
name where a retry copy goes"
)));
}
Ok(RedeliveryAddress::new(subject.to_owned()))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct CoreSubject {
subject: String,
queue_group: Option<String>,
}
impl CoreSubject {
pub fn new(subject: impl Into<String>) -> Self {
Self {
subject: subject.into(),
queue_group: None,
}
}
pub fn queue_group(mut self, name: impl Into<String>) -> Self {
self.queue_group = Some(name.into());
self
}
#[cfg(feature = "asyncapi")]
pub(crate) fn group(&self) -> Option<&str> {
self.queue_group.as_deref()
}
}
impl Sealed for CoreSubject {
fn plan(&self) -> SubscriptionPlan<'_> {
SubscriptionPlan::Core {
queue_group: self.queue_group.as_deref(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct CoreWildcard {
subject: String,
queue_group: Option<String>,
}
impl CoreWildcard {
pub fn new(subject: impl Into<String>) -> Self {
Self {
subject: subject.into(),
queue_group: None,
}
}
pub fn queue_group(mut self, name: impl Into<String>) -> Self {
self.queue_group = Some(name.into());
self
}
#[cfg(feature = "asyncapi")]
pub(crate) fn group(&self) -> Option<&str> {
self.queue_group.as_deref()
}
}
impl Sealed for CoreWildcard {
fn plan(&self) -> SubscriptionPlan<'_> {
SubscriptionPlan::Core {
queue_group: self.queue_group.as_deref(),
}
}
}
impl NatsSubscription for CoreWildcard {
fn subject(&self) -> &str {
&self.subject
}
}
impl NatsSubscription for CoreSubject {
fn subject(&self) -> &str {
&self.subject
}
fn ensure_subject(&self) -> Result<(), NatsError> {
publish_destination(self.subject()).map(|_| ())
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct JetStreamSubject {
subject: String,
stream: String,
durable: Option<String>,
filter_subject: Option<String>,
ack_wait: Option<NonZeroDuration>,
max_ack_pending: Option<i64>,
deliver_policy: Option<DeliverPolicy>,
pull_expires: Option<NonZeroDuration>,
}
impl JetStreamSubject {
pub fn new(subject: impl Into<String>, stream: impl Into<String>) -> Self {
Self {
subject: subject.into(),
stream: stream.into(),
durable: None,
filter_subject: None,
ack_wait: None,
max_ack_pending: None,
deliver_policy: None,
pull_expires: None,
}
}
pub fn durable(mut self, name: impl Into<String>) -> Self {
self.durable = Some(name.into());
self
}
pub fn filter_subject(mut self, subject: impl Into<String>) -> Self {
self.filter_subject = Some(subject.into());
self
}
pub const fn ack_wait(mut self, ack_wait: NonZeroDuration) -> Self {
self.ack_wait = Some(ack_wait);
self
}
pub const fn max_ack_pending(mut self, max: i64) -> Self {
self.max_ack_pending = Some(max);
self
}
pub const fn deliver_policy(mut self, policy: DeliverPolicy) -> Self {
self.deliver_policy = Some(policy);
self
}
pub const fn pull_expires(mut self, expires: NonZeroDuration) -> Self {
self.pull_expires = Some(expires);
self
}
pub(crate) fn consumer_filter(&self) -> &str {
self.filter_subject.as_deref().unwrap_or(&self.subject)
}
#[cfg(feature = "asyncapi")]
fn consumer_bindings(&self) -> Bindings {
let body = JetStreamChannel {
stream: &self.stream,
durable: self.durable.as_deref(),
filter_subject: self.consumer_filter(),
ack_wait_seconds: self
.ack_wait
.map_or(DEFAULT_ACK_WAIT, NonZeroDuration::get)
.as_secs_f64(),
max_ack_pending: self.max_ack_pending.unwrap_or(DEFAULT_MAX_ACK_PENDING),
deliver_policy: deliver_policy_name(self.deliver_policy.unwrap_or(DeliverPolicy::All)),
};
Binding::extension(JETSTREAM_EXTENSION, &body)
.map(|binding| Bindings::new().with(binding))
.unwrap_or_default()
}
}
#[cfg(feature = "asyncapi")]
pub(crate) const JETSTREAM_EXTENSION: &str = "x-ruststream-jetstream";
#[cfg(feature = "asyncapi")]
fn core_operation_bindings(queue_group: Option<&str>) -> Bindings {
queue_group
.map(|queue| NatsOperation { queue })
.and_then(|body| Binding::new("nats", "0.1.0", &body).ok())
.map(|binding| Bindings::new().with(binding))
.unwrap_or_default()
}
#[cfg(feature = "asyncapi")]
fn deliver_policy_name(policy: DeliverPolicy) -> &'static str {
match policy {
DeliverPolicy::All => "all",
DeliverPolicy::Last => "last",
DeliverPolicy::New => "new",
DeliverPolicy::ByStartSequence { .. } => "by_start_sequence",
DeliverPolicy::ByStartTime { .. } => "by_start_time",
DeliverPolicy::LastPerSubject => "last_per_subject",
}
}
#[cfg(feature = "asyncapi")]
#[derive(Serialize)]
struct NatsOperation<'a> {
queue: &'a str,
}
#[cfg(feature = "asyncapi")]
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct JetStreamChannel<'a> {
stream: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
durable: Option<&'a str>,
filter_subject: &'a str,
ack_wait_seconds: f64,
max_ack_pending: i64,
deliver_policy: &'static str,
}
impl Sealed for JetStreamSubject {
fn plan(&self) -> SubscriptionPlan<'_> {
SubscriptionPlan::JetStream {
stream: &self.stream,
durable: self.durable.as_deref(),
filter_subject: self.filter_subject.as_deref().unwrap_or(&self.subject),
ack_wait: self.ack_wait.map_or(DEFAULT_ACK_WAIT, NonZeroDuration::get),
max_ack_pending: self.max_ack_pending.unwrap_or(DEFAULT_MAX_ACK_PENDING),
deliver_policy: self.deliver_policy.unwrap_or(DeliverPolicy::All),
pull_expires: self
.pull_expires
.map_or(DEFAULT_PULL_EXPIRES, NonZeroDuration::get),
}
}
}
impl NatsSubscription for JetStreamSubject {
fn subject(&self) -> &str {
&self.subject
}
}
impl IntoSource for CoreSubject {
type Source = Self;
fn into_source(self) -> Self {
self
}
}
impl IntoSource for JetStreamSubject {
type Source = Self;
fn into_source(self) -> Self {
self
}
}
impl IntoSource for CoreWildcard {
type Source = Self;
fn into_source(self) -> Self {
self
}
}
impl SubscriptionSource<ConnectedNatsBroker> for CoreSubject {
type Subscriber = NatsSubscriber;
type Copies = AddressedCopies;
fn name(&self) -> &str {
NatsSubscription::subject(self)
}
async fn subscribe(
self,
connected: &ConnectedNatsBroker,
) -> Result<Self::Subscriber, NatsError> {
connected.subscribe_with(self).await
}
#[cfg(feature = "asyncapi")]
fn operation_bindings(&self) -> Bindings {
core_operation_bindings(self.group())
}
}
impl RedeliveryAddressed<ConnectedNatsBroker> for CoreSubject {
fn redelivery_address(
&self,
_connected: &ConnectedNatsBroker,
) -> impl Future<Output = Result<RedeliveryAddress, NatsError>> + Send {
ready(publish_destination(NatsSubscription::subject(self)))
}
}
impl SubscriptionSource<ConnectedNatsBroker> for CoreWildcard {
type Subscriber = NatsSubscriber;
type Copies = NamedCopies;
fn name(&self) -> &str {
NatsSubscription::subject(self)
}
async fn subscribe(
self,
connected: &ConnectedNatsBroker,
) -> Result<Self::Subscriber, NatsError> {
connected.subscribe_with(self).await
}
#[cfg(feature = "asyncapi")]
fn operation_bindings(&self) -> Bindings {
core_operation_bindings(self.group())
}
}
impl SubscriptionSource<ConnectedNatsBroker> for JetStreamSubject {
type Subscriber = NatsSubscriber;
type Copies = AddressedCopies;
fn name(&self) -> &str {
NatsSubscription::subject(self)
}
async fn subscribe(
self,
connected: &ConnectedNatsBroker,
) -> Result<Self::Subscriber, NatsError> {
connected.subscribe_with(self).await
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self) -> Bindings {
self.consumer_bindings()
}
}
impl RedeliveryAddressed<ConnectedNatsBroker> for JetStreamSubject {
fn redelivery_address(
&self,
_connected: &ConnectedNatsBroker,
) -> impl Future<Output = Result<RedeliveryAddress, NatsError>> + Send {
ready(Ok(RedeliveryAddress::new(
self.consumer_filter().to_owned(),
)))
}
}
#[cfg(test)]
mod tests {
use ruststream::nonzero;
use super::*;
#[test]
fn a_plain_subscription_is_core_without_a_queue_group() {
let opts = CoreSubject::new("orders.created");
opts.ensure_subject().expect("core ok");
assert!(matches!(
opts.plan(),
SubscriptionPlan::Core { queue_group: None }
));
}
#[test]
fn a_queue_group_reaches_the_broker() {
let opts = CoreSubject::new("orders.created").queue_group("workers");
assert!(matches!(
opts.plan(),
SubscriptionPlan::Core {
queue_group: Some("workers")
}
));
}
#[test]
fn jetstream_defaults_are_resolved_in_the_plan() {
let consumer = JetStreamSubject::new("orders.*", "ORDERS");
let SubscriptionPlan::JetStream {
stream,
durable,
filter_subject,
ack_wait,
max_ack_pending,
pull_expires,
..
} = consumer.plan()
else {
panic!("a jetstream descriptor must plan a jetstream subscription");
};
assert_eq!(stream, "ORDERS");
assert_eq!(durable, None);
assert_eq!(filter_subject, "orders.*");
assert_eq!(ack_wait, DEFAULT_ACK_WAIT);
assert_eq!(max_ack_pending, DEFAULT_MAX_ACK_PENDING);
assert_eq!(pull_expires, DEFAULT_PULL_EXPIRES);
}
#[test]
fn only_a_concrete_subject_is_a_redelivery_address() {
assert_eq!(
publish_destination("orders.created").expect("a literal subject is a destination"),
RedeliveryAddress::new("orders.created")
);
for pattern in ["orders.*", "orders.>", ">", ""] {
assert!(
CoreSubject::new(pattern).ensure_subject().is_err(),
"`{pattern}` is not a destination, so CoreSubject must refuse it"
);
assert!(publish_destination(pattern).is_err());
}
}
#[test]
fn a_pattern_opens_as_a_wildcard_subscription() {
for pattern in ["orders.*", "orders.>", ">", "orders.created"] {
CoreWildcard::new(pattern)
.ensure_subject()
.expect("a pattern is what this descriptor is for");
}
assert!(CoreWildcard::new("").ensure_subject().is_err());
}
#[test]
fn a_jetstream_consumer_reports_the_subject_it_actually_reads() {
assert_eq!(
JetStreamSubject::new("orders.*", "ORDERS")
.filter_subject("orders.created")
.consumer_filter(),
"orders.created"
);
assert_eq!(
JetStreamSubject::new("orders.*", "ORDERS").consumer_filter(),
"orders.*"
);
}
#[test]
fn jetstream_settings_reach_the_plan() {
let consumer = JetStreamSubject::new("orders.*", "ORDERS")
.durable("worker")
.filter_subject("orders.created")
.ack_wait(NonZeroDuration::from_secs(nonzero!(5)))
.max_ack_pending(64)
.deliver_policy(DeliverPolicy::New)
.pull_expires(NonZeroDuration::from_millis(nonzero!(250)));
let SubscriptionPlan::JetStream {
durable,
filter_subject,
ack_wait,
max_ack_pending,
pull_expires,
..
} = consumer.plan()
else {
panic!("a jetstream descriptor must plan a jetstream subscription");
};
assert_eq!(durable, Some("worker"));
assert_eq!(filter_subject, "orders.created");
assert_eq!(ack_wait, Duration::from_secs(5));
assert_eq!(max_ack_pending, 64);
assert_eq!(pull_expires, Duration::from_millis(250));
}
#[test]
fn an_empty_subject_is_rejected_on_both_descriptors() {
let core = CoreSubject::new("").ensure_subject().unwrap_err();
assert!(matches!(core, NatsError::InvalidOptions(msg) if msg.contains("subject")));
let js = JetStreamSubject::new("", "ORDERS")
.ensure_subject()
.unwrap_err();
assert!(matches!(js, NatsError::InvalidOptions(msg) if msg.contains("subject")));
}
#[test]
fn a_zero_duration_has_no_non_zero_form() {
assert!(NonZeroDuration::new(Duration::ZERO).is_none());
assert_eq!(
NonZeroDuration::new(Duration::from_millis(1)).map(NonZeroDuration::get),
Some(Duration::from_millis(1))
);
}
}