Skip to main content

ruststream_nats/
subject.rs

1//! The subject a service subscribes to: [`CoreSubject`] and [`CoreWildcard`] over Core NATS,
2//! [`JetStreamSubject`] through a `JetStream` pull consumer.
3//!
4//! They are the crate's [`SubscriptionSource`]s: the value a `#[subscriber(..)]` handler carries,
5//! and the value the runtime resolves once against the connected broker at startup.
6//!
7//! One type per delivery model, because the two models share no settings. A queue group is Core
8//! NATS only and a durable name describes a `JetStream` consumer, so each is a method on one type
9//! and absent from the other. Naming the wrong one is a compile error rather than a service that
10//! starts and then refuses its own subscription.
11//!
12//! Core NATS then splits once more, over the one property a retry depends on: one subject is a
13//! destination a copy of a delivery can be published to, a pattern is not. [`CoreSubject`] carries
14//! the first and answers the framework with its own subject; [`CoreWildcard`] carries the second
15//! and leaves the mount site to name where a copy goes.
16
17use std::future::{Future, ready};
18use std::num::NonZeroU64;
19use std::time::Duration;
20
21pub use async_nats::jetstream::consumer::DeliverPolicy;
22#[cfg(feature = "asyncapi")]
23use ruststream::asyncapi::{Binding, Bindings};
24use ruststream::runtime::IntoSource;
25use ruststream::{
26    AddressedCopies, NamedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource,
27};
28
29#[cfg(feature = "asyncapi")]
30use serde::Serialize;
31
32use self::sealed::Sealed;
33use crate::{ConnectedNatsBroker, error::NatsError, subscriber::NatsSubscriber};
34
35/// Ack window a `JetStream` consumer gets when the descriptor names none.
36const DEFAULT_ACK_WAIT: Duration = Duration::from_secs(30);
37/// In-flight unacked deliveries a `JetStream` consumer allows when the descriptor names no cap.
38const DEFAULT_MAX_ACK_PENDING: i64 = 1024;
39/// Fetch window a `JetStream` pull consumer uses when the descriptor names none.
40const DEFAULT_PULL_EXPIRES: Duration = Duration::from_secs(5);
41
42/// A [`Duration`] that is known not to be zero.
43///
44/// [`JetStreamSubject::pull_expires`] and [`JetStreamSubject::ack_wait`] take one. A fetch that
45/// expires the instant it is issued turns the batch loop into a hot spin, so the zero is kept out
46/// of the type instead of being caught when the subscription opens.
47///
48/// # Examples
49///
50/// ```
51/// # mod demo {
52/// use ruststream_nats::prelude::*;
53/// use serde::Deserialize;
54///
55/// #[derive(Deserialize)]
56/// struct Order {
57///     amount: u64,
58/// }
59///
60/// #[subscriber(
61///     JetStreamSubject::new("orders.*", "ORDERS")
62///         .durable("reconciler")
63///         .ack_wait(NonZeroDuration::from_secs(nonzero!(60)))
64///         .pull_expires(NonZeroDuration::from_millis(nonzero!(300)))
65/// )]
66/// async fn reconcile(orders: &[Order]) -> HandlerOutcome {
67///     let total: u64 = orders.iter().map(|order| order.amount).sum();
68///     println!("reconciling {} orders worth {total}", orders.len());
69///     HandlerOutcome::ack()
70/// }
71///
72/// #[ruststream::app]
73/// fn app() -> impl App {
74///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
75///         NatsBroker::new("nats://localhost:4222"),
76///         |b| {
77///             b.include(reconcile.batch(nonzero!(50)));
78///         },
79///     )
80/// }
81/// # }
82/// # fn main() {}
83/// ```
84///
85/// A literal is checked while the crate compiles, so a zero window does not build:
86///
87/// ```compile_fail
88/// use ruststream::nonzero;
89/// use ruststream_nats::NonZeroDuration;
90///
91/// let window = NonZeroDuration::from_millis(nonzero!(0));
92/// ```
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
94#[must_use]
95pub struct NonZeroDuration(Duration);
96
97impl NonZeroDuration {
98    /// Wraps `value`, or returns `None` when it is zero.
99    ///
100    /// This is the entry for a duration that arrives at run time, from configuration or from a
101    /// calculation. A literal reads better through [`from_secs`](Self::from_secs) or
102    /// [`from_millis`](Self::from_millis).
103    #[must_use]
104    pub const fn new(value: Duration) -> Option<Self> {
105        if value.is_zero() {
106            None
107        } else {
108            Some(Self(value))
109        }
110    }
111
112    /// Builds a duration of `secs` seconds.
113    pub const fn from_secs(secs: NonZeroU64) -> Self {
114        Self(Duration::from_secs(secs.get()))
115    }
116
117    /// Builds a duration of `millis` milliseconds.
118    pub const fn from_millis(millis: NonZeroU64) -> Self {
119        Self(Duration::from_millis(millis.get()))
120    }
121
122    /// The duration itself.
123    #[must_use]
124    pub const fn get(self) -> Duration {
125        self.0
126    }
127}
128
129impl From<NonZeroDuration> for Duration {
130    fn from(value: NonZeroDuration) -> Self {
131        value.get()
132    }
133}
134
135/// What a descriptor asks the connected broker to open, with every default already resolved.
136///
137/// Reachable through the sealed [`NatsSubscription`], so it is `pub` for the visibility checker
138/// only: `subject` is a private module and nothing re-exports this, so no crate outside
139/// can name it.
140#[doc(hidden)]
141#[derive(Debug)]
142pub enum SubscriptionPlan<'a> {
143    Core {
144        queue_group: Option<&'a str>,
145    },
146    JetStream {
147        stream: &'a str,
148        durable: Option<&'a str>,
149        filter_subject: &'a str,
150        ack_wait: Duration,
151        max_ack_pending: i64,
152        deliver_policy: DeliverPolicy,
153        pull_expires: Duration,
154    },
155}
156
157mod sealed {
158    use super::SubscriptionPlan;
159
160    pub trait Sealed {
161        fn plan(&self) -> SubscriptionPlan<'_>;
162    }
163}
164
165/// A NATS subscription descriptor: [`CoreSubject`], [`CoreWildcard`] or [`JetStreamSubject`].
166///
167/// Sealed, because the two delivery models NATS has are the two this crate ships.
168#[diagnostic::on_unimplemented(
169    message = "`{Self}` does not describe a NATS subscription",
170    label = "not a subscription descriptor",
171    note = "use `CoreSubject::new(subject)` for one Core NATS subject, \
172            `CoreWildcard::new(pattern)` for a Core NATS pattern, or \
173            `JetStreamSubject::new(subject, stream)` for a JetStream pull consumer"
174)]
175pub trait NatsSubscription: Sealed {
176    /// The subject pattern this subscription receives messages on.
177    fn subject(&self) -> &str;
178
179    /// The one thing about a subscription the types cannot settle: the subject is a string the
180    /// caller supplies, often from configuration, and a descriptor constructor is the expression
181    /// a `#[subscriber(..)]` attribute writes, so it cannot hand back a `Result`. Every
182    /// `subscribe` implementation calls this before it does any work.
183    ///
184    /// # Errors
185    ///
186    /// Returns [`NatsError::InvalidOptions`] when the subject is empty.
187    fn ensure_subject(&self) -> Result<(), NatsError> {
188        if self.subject().is_empty() {
189            return Err(NatsError::InvalidOptions(
190                "subject must be non-empty".into(),
191            ));
192        }
193        Ok(())
194    }
195}
196
197/// Whether `subject` carries a NATS wildcard: `*` for one token, `>` for the rest of them.
198///
199/// The server matches a wildcard on delivery and refuses it on publish, so a subject that carries
200/// one is a subscription pattern and not a destination.
201fn has_wildcard(subject: &str) -> bool {
202    subject.split('.').any(|token| token == "*" || token == ">")
203}
204
205/// The subject as a publish destination.
206///
207/// # Errors
208///
209/// Returns [`NatsError::InvalidOptions`] when the subject is empty or carries a wildcard.
210pub(crate) fn publish_destination(subject: &str) -> Result<RedeliveryAddress, NatsError> {
211    if subject.is_empty() {
212        return Err(NatsError::InvalidOptions(
213            "subject must be non-empty".into(),
214        ));
215    }
216    if has_wildcard(subject) {
217        return Err(NatsError::InvalidOptions(format!(
218            "`{subject}` is a subscription pattern, and a NATS server refuses a publish to `*` \
219             or `>`. Subscribe to a pattern with `CoreWildcard`, which leaves the mount site to \
220             name where a retry copy goes"
221        )));
222    }
223    Ok(RedeliveryAddress::new(subject.to_owned()))
224}
225
226/// One subject subscribed to over Core NATS.
227///
228/// Core NATS delivers to whoever is subscribed at that moment and stores nothing, so the only
229/// setting it has is the queue group that load-balances a subject across several subscribers.
230/// Reading a stream instead is [`JetStreamSubject`]; reading many subjects at once is
231/// [`CoreWildcard`].
232///
233/// The subject is one destination, so a delayed redelivery needs nothing from the mount site: the
234/// framework publishes its copy back to this very subject.
235///
236/// # Examples
237///
238/// ```
239/// # mod demo {
240/// use ruststream_nats::prelude::*;
241/// use serde::Deserialize;
242///
243/// #[derive(Deserialize)]
244/// struct Order {
245///     id: u64,
246/// }
247///
248/// // Every replica joins `workers`, so each order reaches one of them.
249/// #[subscriber(CoreSubject::new("orders.created").queue_group("workers"))]
250/// async fn fulfil(order: &Order) -> HandlerOutcome {
251///     println!("fulfilling order {}", order.id);
252///     HandlerOutcome::ack()
253/// }
254///
255/// #[ruststream::app]
256/// fn app() -> impl App {
257///     RustStream::new(AppInfo::new("fulfillment", "0.1.0")).with_broker(
258///         NatsBroker::new("nats://localhost:4222"),
259///         |b| {
260///             b.include(fulfil);
261///         },
262///     )
263/// }
264/// # }
265/// # fn main() {}
266/// ```
267///
268/// A `JetStream` setting is not a method here, so asking for one does not compile:
269///
270/// ```compile_fail
271/// use ruststream_nats::CoreSubject;
272///
273/// // `durable` names a JetStream consumer, and Core NATS has none.
274/// let bad = CoreSubject::new("orders.created").durable("worker-1");
275/// ```
276#[derive(Debug, Clone, PartialEq, Eq)]
277#[must_use]
278pub struct CoreSubject {
279    subject: String,
280    queue_group: Option<String>,
281}
282
283impl CoreSubject {
284    /// Subscribes to `subject` over Core NATS.
285    pub fn new(subject: impl Into<String>) -> Self {
286        Self {
287            subject: subject.into(),
288            queue_group: None,
289        }
290    }
291
292    /// Load-balances the subject across a queue group: the server hands each message to one
293    /// member instead of all of them.
294    pub fn queue_group(mut self, name: impl Into<String>) -> Self {
295        self.queue_group = Some(name.into());
296        self
297    }
298
299    /// The queue group this subscription joins, if any.
300    #[cfg(feature = "asyncapi")]
301    pub(crate) fn group(&self) -> Option<&str> {
302        self.queue_group.as_deref()
303    }
304}
305
306impl Sealed for CoreSubject {
307    fn plan(&self) -> SubscriptionPlan<'_> {
308        SubscriptionPlan::Core {
309            queue_group: self.queue_group.as_deref(),
310        }
311    }
312}
313
314/// A pattern subscribed to over Core NATS: `orders.*`, `orders.>`, `>`.
315///
316/// The same subscription as [`CoreSubject`], on many subjects instead of one. That is the whole
317/// difference between the two types, and it is the difference that matters to a retry: a pattern
318/// is matched on delivery and refused on publish, so this subscription cannot say where a copy of
319/// a delivery would reach it again. The mount site says it instead, with
320/// `.out_retry(Publish).to("orders.created")` or with a transform that reads the delivery and
321/// names its own subject; a registration that says neither refuses to start.
322///
323/// # Examples
324///
325/// ```
326/// # mod demo {
327/// use std::time::Duration;
328///
329/// use ruststream_nats::prelude::*;
330/// use serde::Deserialize;
331///
332/// #[derive(Deserialize)]
333/// struct Order {
334///     id: u64,
335///     paid: bool,
336/// }
337///
338/// #[subscriber(CoreWildcard::new("orders.*").queue_group("auditors"))]
339/// async fn audit(order: &Order) -> HandlerOutcome {
340///     if order.paid {
341///         return HandlerOutcome::ack();
342///     }
343///     HandlerOutcome::retry_after(Duration::from_secs(30))
344/// }
345///
346/// #[ruststream::app]
347/// fn app() -> impl App {
348///     RustStream::new(AppInfo::new("audit", "0.1.0")).with_broker(
349///         NatsBroker::new("nats://localhost:4222"),
350///         |b| {
351///             // A pattern is no destination, so the mount site names where a delayed copy goes.
352///             b.include(audit)
353///                 .max_attempts(nonzero!(5u32))
354///                 .dead_letter("orders.dead")
355///                 .out_retry(Publish)
356///                 .to("orders.audit");
357///         },
358///     )
359/// }
360/// # }
361/// # fn main() {}
362/// ```
363#[derive(Debug, Clone, PartialEq, Eq)]
364#[must_use]
365pub struct CoreWildcard {
366    subject: String,
367    queue_group: Option<String>,
368}
369
370impl CoreWildcard {
371    /// Subscribes to the pattern `subject` over Core NATS.
372    pub fn new(subject: impl Into<String>) -> Self {
373        Self {
374            subject: subject.into(),
375            queue_group: None,
376        }
377    }
378
379    /// Load-balances the pattern across a queue group, exactly as
380    /// [`CoreSubject::queue_group`] does for one subject.
381    pub fn queue_group(mut self, name: impl Into<String>) -> Self {
382        self.queue_group = Some(name.into());
383        self
384    }
385
386    /// The queue group this subscription joins, if any.
387    #[cfg(feature = "asyncapi")]
388    pub(crate) fn group(&self) -> Option<&str> {
389        self.queue_group.as_deref()
390    }
391}
392
393impl Sealed for CoreWildcard {
394    fn plan(&self) -> SubscriptionPlan<'_> {
395        SubscriptionPlan::Core {
396            queue_group: self.queue_group.as_deref(),
397        }
398    }
399}
400
401impl NatsSubscription for CoreWildcard {
402    fn subject(&self) -> &str {
403        &self.subject
404    }
405}
406
407impl NatsSubscription for CoreSubject {
408    fn subject(&self) -> &str {
409        &self.subject
410    }
411
412    /// A wildcard belongs to [`CoreWildcard`], so it is refused here rather than opened as a
413    /// subscription this type promises to address.
414    ///
415    /// The subject is a string the caller supplies, often from configuration, so the two forms
416    /// cannot be told apart while the crate compiles. The subscription refuses to open instead,
417    /// which is a service that does not start rather than one that starts and loses a delayed
418    /// message.
419    fn ensure_subject(&self) -> Result<(), NatsError> {
420        publish_destination(self.subject()).map(|_| ())
421    }
422}
423
424/// A subject read through a `JetStream` pull consumer on one stream.
425///
426/// The consumer is created when the subscription opens. Name it with [`durable`](Self::durable)
427/// and the server keeps its position across restarts; leave the name out and the consumer is
428/// ephemeral, discarded when the subscriber drops.
429///
430/// # Examples
431///
432/// ```
433/// # mod demo {
434/// use ruststream_nats::prelude::*;
435/// use serde::Deserialize;
436///
437/// #[derive(Deserialize)]
438/// struct Order {
439///     id: u64,
440/// }
441///
442/// // The durable keeps its position across restarts, and every replica shares it.
443/// #[subscriber(
444///     JetStreamSubject::new("orders.*", "ORDERS")
445///         .durable("billing")
446///         .ack_wait(NonZeroDuration::from_secs(nonzero!(30)))
447/// )]
448/// async fn bill(order: &Order) -> HandlerOutcome {
449///     println!("billing order {}", order.id);
450///     HandlerOutcome::ack()
451/// }
452///
453/// #[ruststream::app]
454/// fn app() -> impl App {
455///     RustStream::new(AppInfo::new("billing", "0.1.0")).with_broker(
456///         NatsBroker::new("nats://localhost:4222"),
457///         |b| {
458///             b.include(bill);
459///         },
460///     )
461/// }
462/// # }
463/// # fn main() {}
464/// ```
465///
466/// A queue group is Core NATS only, so it is not a method here:
467///
468/// ```compile_fail
469/// use ruststream_nats::JetStreamSubject;
470///
471/// let bad = JetStreamSubject::new("orders.*", "ORDERS").queue_group("workers");
472/// ```
473#[derive(Debug, Clone)]
474#[must_use]
475pub struct JetStreamSubject {
476    subject: String,
477    stream: String,
478    durable: Option<String>,
479    filter_subject: Option<String>,
480    ack_wait: Option<NonZeroDuration>,
481    max_ack_pending: Option<i64>,
482    deliver_policy: Option<DeliverPolicy>,
483    pull_expires: Option<NonZeroDuration>,
484}
485
486impl JetStreamSubject {
487    /// Reads `subject` from the stream named `stream`.
488    ///
489    /// `subject` is the pattern the subscription reports and the default consumer filter;
490    /// `stream` is the `JetStream` stream that stores it.
491    pub fn new(subject: impl Into<String>, stream: impl Into<String>) -> Self {
492        Self {
493            subject: subject.into(),
494            stream: stream.into(),
495            durable: None,
496            filter_subject: None,
497            ack_wait: None,
498            max_ack_pending: None,
499            deliver_policy: None,
500            pull_expires: None,
501        }
502    }
503
504    /// Names the consumer, so the server keeps its position across restarts.
505    pub fn durable(mut self, name: impl Into<String>) -> Self {
506        self.durable = Some(name.into());
507        self
508    }
509
510    /// Narrows which subjects of the stream this consumer reads. Defaults to the subject the
511    /// constructor named.
512    pub fn filter_subject(mut self, subject: impl Into<String>) -> Self {
513        self.filter_subject = Some(subject.into());
514        self
515    }
516
517    /// Per-message acknowledgement timeout. After this window an unacked delivery is redelivered.
518    /// Defaults to 30 seconds.
519    pub const fn ack_wait(mut self, ack_wait: NonZeroDuration) -> Self {
520        self.ack_wait = Some(ack_wait);
521        self
522    }
523
524    /// Soft cap on in-flight unacked deliveries. Defaults to 1024.
525    pub const fn max_ack_pending(mut self, max: i64) -> Self {
526        self.max_ack_pending = Some(max);
527        self
528    }
529
530    /// Where a newly created consumer starts reading. Defaults to `DeliverPolicy::All`.
531    pub const fn deliver_policy(mut self, policy: DeliverPolicy) -> Self {
532        self.deliver_policy = Some(policy);
533        self
534    }
535
536    /// How long one fetch waits before delivering a partial (or retrying an empty) batch.
537    /// Defaults to 5 seconds. Has no effect on the per-message
538    /// [`Subscriber::stream`](ruststream::Subscriber::stream) path.
539    ///
540    /// How many messages a batch carries is not set here: the batch size travels with the
541    /// registration (`b.include(handler.batch(nonzero!(6)))`) and reaches the fetch as the
542    /// argument of [`BatchSubscriber::batches`](ruststream::BatchSubscriber::batches).
543    pub const fn pull_expires(mut self, expires: NonZeroDuration) -> Self {
544        self.pull_expires = Some(expires);
545        self
546    }
547
548    /// The subjects the consumer actually reads: the filter where one is set, the subject
549    /// otherwise.
550    pub(crate) fn consumer_filter(&self) -> &str {
551        self.filter_subject.as_deref().unwrap_or(&self.subject)
552    }
553
554    /// What this consumer is, in `JetStream`'s own words.
555    ///
556    /// The `nats` binding has one field and it belongs to a Core NATS queue group, so everything
557    /// a stream consumer is configured with goes under an extension of this crate's own. Every
558    /// value is the descriptor's; nothing here waits for a connection.
559    #[cfg(feature = "asyncapi")]
560    fn consumer_bindings(&self) -> Bindings {
561        let body = JetStreamChannel {
562            stream: &self.stream,
563            durable: self.durable.as_deref(),
564            filter_subject: self.consumer_filter(),
565            ack_wait_seconds: self
566                .ack_wait
567                .map_or(DEFAULT_ACK_WAIT, NonZeroDuration::get)
568                .as_secs_f64(),
569            max_ack_pending: self.max_ack_pending.unwrap_or(DEFAULT_MAX_ACK_PENDING),
570            deliver_policy: deliver_policy_name(self.deliver_policy.unwrap_or(DeliverPolicy::All)),
571        };
572        Binding::extension(JETSTREAM_EXTENSION, &body)
573            .map(|binding| Bindings::new().with(binding))
574            .unwrap_or_default()
575    }
576}
577
578/// The extension key the `JetStream` consumer settings travel under.
579///
580/// The specification's `nats` binding declares its channel object empty, and the protocol keys are
581/// a closed list, so a stream, a durable name and an ack window have no lawful binding field. An
582/// `x-` extension sits at the same level and carries no `bindingVersion`.
583#[cfg(feature = "asyncapi")]
584pub(crate) const JETSTREAM_EXTENSION: &str = "x-ruststream-jetstream";
585
586/// The whole `nats` binding, version 0.1.0: the queue group of a Core NATS subscription, on the
587/// `receive` operation. A subscription outside a group contributes nothing rather than an empty
588/// object.
589#[cfg(feature = "asyncapi")]
590fn core_operation_bindings(queue_group: Option<&str>) -> Bindings {
591    queue_group
592        .map(|queue| NatsOperation { queue })
593        .and_then(|body| Binding::new("nats", "0.1.0", &body).ok())
594        .map(|binding| Bindings::new().with(binding))
595        .unwrap_or_default()
596}
597
598/// The name `JetStream` gives a delivery policy on the wire.
599#[cfg(feature = "asyncapi")]
600fn deliver_policy_name(policy: DeliverPolicy) -> &'static str {
601    match policy {
602        DeliverPolicy::All => "all",
603        DeliverPolicy::Last => "last",
604        DeliverPolicy::New => "new",
605        DeliverPolicy::ByStartSequence { .. } => "by_start_sequence",
606        DeliverPolicy::ByStartTime { .. } => "by_start_time",
607        DeliverPolicy::LastPerSubject => "last_per_subject",
608    }
609}
610
611/// The queue group, which is the whole `nats` binding.
612#[cfg(feature = "asyncapi")]
613#[derive(Serialize)]
614struct NatsOperation<'a> {
615    queue: &'a str,
616}
617
618/// What a `JetStream` consumer is configured with, under this crate's extension key.
619#[cfg(feature = "asyncapi")]
620#[derive(Serialize)]
621#[serde(rename_all = "camelCase")]
622struct JetStreamChannel<'a> {
623    stream: &'a str,
624    #[serde(skip_serializing_if = "Option::is_none")]
625    durable: Option<&'a str>,
626    filter_subject: &'a str,
627    ack_wait_seconds: f64,
628    max_ack_pending: i64,
629    deliver_policy: &'static str,
630}
631
632impl Sealed for JetStreamSubject {
633    fn plan(&self) -> SubscriptionPlan<'_> {
634        SubscriptionPlan::JetStream {
635            stream: &self.stream,
636            durable: self.durable.as_deref(),
637            filter_subject: self.filter_subject.as_deref().unwrap_or(&self.subject),
638            ack_wait: self.ack_wait.map_or(DEFAULT_ACK_WAIT, NonZeroDuration::get),
639            max_ack_pending: self.max_ack_pending.unwrap_or(DEFAULT_MAX_ACK_PENDING),
640            deliver_policy: self.deliver_policy.unwrap_or(DeliverPolicy::All),
641            pull_expires: self
642                .pull_expires
643                .map_or(DEFAULT_PULL_EXPIRES, NonZeroDuration::get),
644        }
645    }
646}
647
648impl NatsSubscription for JetStreamSubject {
649    fn subject(&self) -> &str {
650        &self.subject
651    }
652}
653
654/// A descriptor is already a source, so the macro-free constructor takes it as it stands:
655/// `subscriber(JetStreamSubject::new("orders.*", "ORDERS"), body)` names the same subscription
656/// the `#[subscriber(..)]` attribute does.
657///
658/// # Examples
659///
660/// ```
661/// # mod demo {
662/// use ruststream::schemars::JsonSchema;
663/// use ruststream_nats::prelude::*;
664/// use serde::Deserialize;
665///
666/// #[derive(Deserialize, JsonSchema)]
667/// # #[schemars(crate = "ruststream::schemars")]
668/// struct Order {
669///     id: u64,
670/// }
671///
672/// struct Audit;
673///
674/// impl Handle<Order> for Audit {
675///     async fn handle(
676///         &self,
677///         order: &Order,
678///         _outs: &(),
679///         _ctx: &mut Context<'_>,
680///     ) -> Result<(), HandlerOutcome> {
681///         println!("auditing order {}", order.id);
682///         Ok(())
683///     }
684/// }
685///
686/// #[ruststream::app]
687/// fn app() -> impl App {
688///     RustStream::new(AppInfo::new("audit", "0.1.0")).with_broker(
689///         NatsBroker::new("nats://localhost:4222"),
690///         |b| {
691///             let audited = CoreSubject::new("orders.created").queue_group("auditors");
692///             b.include(subscriber(audited, Audit).build());
693///         },
694///     )
695/// }
696/// # }
697/// # fn main() {}
698/// ```
699impl IntoSource for CoreSubject {
700    type Source = Self;
701
702    fn into_source(self) -> Self {
703        self
704    }
705}
706
707impl IntoSource for JetStreamSubject {
708    type Source = Self;
709
710    fn into_source(self) -> Self {
711        self
712    }
713}
714
715impl IntoSource for CoreWildcard {
716    type Source = Self;
717
718    fn into_source(self) -> Self {
719        self
720    }
721}
722
723impl SubscriptionSource<ConnectedNatsBroker> for CoreSubject {
724    type Subscriber = NatsSubscriber;
725    // One subject, published to and subscribed to under one name, so the framework's deferred
726    // copy needs nothing from the mount site.
727    type Copies = AddressedCopies;
728
729    fn name(&self) -> &str {
730        NatsSubscription::subject(self)
731    }
732
733    async fn subscribe(
734        self,
735        connected: &ConnectedNatsBroker,
736    ) -> Result<Self::Subscriber, NatsError> {
737        connected.subscribe_with(self).await
738    }
739
740    #[cfg(feature = "asyncapi")]
741    fn operation_bindings(&self) -> Bindings {
742        core_operation_bindings(self.group())
743    }
744}
745
746/// The subject itself, so the answer needs no connection and the future is ready.
747impl RedeliveryAddressed<ConnectedNatsBroker> for CoreSubject {
748    fn redelivery_address(
749        &self,
750        _connected: &ConnectedNatsBroker,
751    ) -> impl Future<Output = Result<RedeliveryAddress, NatsError>> + Send {
752        ready(publish_destination(NatsSubscription::subject(self)))
753    }
754}
755
756impl SubscriptionSource<ConnectedNatsBroker> for CoreWildcard {
757    type Subscriber = NatsSubscriber;
758    // A pattern reads many subjects and names none of them.
759    type Copies = NamedCopies;
760
761    fn name(&self) -> &str {
762        NatsSubscription::subject(self)
763    }
764
765    async fn subscribe(
766        self,
767        connected: &ConnectedNatsBroker,
768    ) -> Result<Self::Subscriber, NatsError> {
769        connected.subscribe_with(self).await
770    }
771
772    #[cfg(feature = "asyncapi")]
773    fn operation_bindings(&self) -> Bindings {
774        core_operation_bindings(self.group())
775    }
776}
777
778impl SubscriptionSource<ConnectedNatsBroker> for JetStreamSubject {
779    type Subscriber = NatsSubscriber;
780    // The consumer reads its filter subject, and a publish there reaches it.
781    type Copies = AddressedCopies;
782
783    fn name(&self) -> &str {
784        NatsSubscription::subject(self)
785    }
786
787    async fn subscribe(
788        self,
789        connected: &ConnectedNatsBroker,
790    ) -> Result<Self::Subscriber, NatsError> {
791        connected.subscribe_with(self).await
792    }
793
794    #[cfg(feature = "asyncapi")]
795    fn channel_bindings(&self) -> Bindings {
796        self.consumer_bindings()
797    }
798}
799
800/// The consumer's filter subject, which defaults to the subject the descriptor names.
801///
802/// A `JetStream` consumer holds a delivery itself and takes the delay in its negative
803/// acknowledgement, so the framework publishes no copy here and this address is what the
804/// conformance suite checks rather than what a running service uses. A consumer filtering a
805/// pattern reports that pattern, which is not a publish destination; nothing publishes to it, and
806/// a `.to(name)` at the mount site overrides it.
807impl RedeliveryAddressed<ConnectedNatsBroker> for JetStreamSubject {
808    fn redelivery_address(
809        &self,
810        _connected: &ConnectedNatsBroker,
811    ) -> impl Future<Output = Result<RedeliveryAddress, NatsError>> + Send {
812        ready(Ok(RedeliveryAddress::new(
813            self.consumer_filter().to_owned(),
814        )))
815    }
816}
817
818#[cfg(test)]
819mod tests {
820    use ruststream::nonzero;
821
822    use super::*;
823
824    #[test]
825    fn a_plain_subscription_is_core_without_a_queue_group() {
826        let opts = CoreSubject::new("orders.created");
827        opts.ensure_subject().expect("core ok");
828        assert!(matches!(
829            opts.plan(),
830            SubscriptionPlan::Core { queue_group: None }
831        ));
832    }
833
834    #[test]
835    fn a_queue_group_reaches_the_broker() {
836        let opts = CoreSubject::new("orders.created").queue_group("workers");
837        assert!(matches!(
838            opts.plan(),
839            SubscriptionPlan::Core {
840                queue_group: Some("workers")
841            }
842        ));
843    }
844
845    #[test]
846    fn jetstream_defaults_are_resolved_in_the_plan() {
847        let consumer = JetStreamSubject::new("orders.*", "ORDERS");
848        let SubscriptionPlan::JetStream {
849            stream,
850            durable,
851            filter_subject,
852            ack_wait,
853            max_ack_pending,
854            pull_expires,
855            ..
856        } = consumer.plan()
857        else {
858            panic!("a jetstream descriptor must plan a jetstream subscription");
859        };
860        assert_eq!(stream, "ORDERS");
861        assert_eq!(durable, None);
862        // The filter defaults to the subject, which only the descriptor knows.
863        assert_eq!(filter_subject, "orders.*");
864        assert_eq!(ack_wait, DEFAULT_ACK_WAIT);
865        assert_eq!(max_ack_pending, DEFAULT_MAX_ACK_PENDING);
866        assert_eq!(pull_expires, DEFAULT_PULL_EXPIRES);
867    }
868
869    // What a reported address promises is that a publish there arrives, so a pattern the server
870    // would refuse a publish to has no business being one. `CoreSubject` carries the subject that
871    // is a destination, and refuses to open on anything else.
872    #[test]
873    fn only_a_concrete_subject_is_a_redelivery_address() {
874        assert_eq!(
875            publish_destination("orders.created").expect("a literal subject is a destination"),
876            RedeliveryAddress::new("orders.created")
877        );
878        for pattern in ["orders.*", "orders.>", ">", ""] {
879            assert!(
880                CoreSubject::new(pattern).ensure_subject().is_err(),
881                "`{pattern}` is not a destination, so CoreSubject must refuse it"
882            );
883            assert!(publish_destination(pattern).is_err());
884        }
885    }
886
887    // A pattern is the other type's business, and it opens on exactly what Core NATS accepts.
888    #[test]
889    fn a_pattern_opens_as_a_wildcard_subscription() {
890        for pattern in ["orders.*", "orders.>", ">", "orders.created"] {
891            CoreWildcard::new(pattern)
892                .ensure_subject()
893                .expect("a pattern is what this descriptor is for");
894        }
895        assert!(CoreWildcard::new("").ensure_subject().is_err());
896    }
897
898    // A JetStream consumer reads its filter, so a narrower filter is the address, not the subject
899    // the descriptor was constructed with.
900    #[test]
901    fn a_jetstream_consumer_reports_the_subject_it_actually_reads() {
902        assert_eq!(
903            JetStreamSubject::new("orders.*", "ORDERS")
904                .filter_subject("orders.created")
905                .consumer_filter(),
906            "orders.created"
907        );
908        assert_eq!(
909            JetStreamSubject::new("orders.*", "ORDERS").consumer_filter(),
910            "orders.*"
911        );
912    }
913
914    #[test]
915    fn jetstream_settings_reach_the_plan() {
916        let consumer = JetStreamSubject::new("orders.*", "ORDERS")
917            .durable("worker")
918            .filter_subject("orders.created")
919            .ack_wait(NonZeroDuration::from_secs(nonzero!(5)))
920            .max_ack_pending(64)
921            .deliver_policy(DeliverPolicy::New)
922            .pull_expires(NonZeroDuration::from_millis(nonzero!(250)));
923        let SubscriptionPlan::JetStream {
924            durable,
925            filter_subject,
926            ack_wait,
927            max_ack_pending,
928            pull_expires,
929            ..
930        } = consumer.plan()
931        else {
932            panic!("a jetstream descriptor must plan a jetstream subscription");
933        };
934        assert_eq!(durable, Some("worker"));
935        assert_eq!(filter_subject, "orders.created");
936        assert_eq!(ack_wait, Duration::from_secs(5));
937        assert_eq!(max_ack_pending, 64);
938        assert_eq!(pull_expires, Duration::from_millis(250));
939    }
940
941    #[test]
942    fn an_empty_subject_is_rejected_on_both_descriptors() {
943        let core = CoreSubject::new("").ensure_subject().unwrap_err();
944        assert!(matches!(core, NatsError::InvalidOptions(msg) if msg.contains("subject")));
945        let js = JetStreamSubject::new("", "ORDERS")
946            .ensure_subject()
947            .unwrap_err();
948        assert!(matches!(js, NatsError::InvalidOptions(msg) if msg.contains("subject")));
949    }
950
951    #[test]
952    fn a_zero_duration_has_no_non_zero_form() {
953        assert!(NonZeroDuration::new(Duration::ZERO).is_none());
954        assert_eq!(
955            NonZeroDuration::new(Duration::from_millis(1)).map(NonZeroDuration::get),
956            Some(Duration::from_millis(1))
957        );
958    }
959}