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///     tracing::info!(count = orders.len(), total, "reconciled");
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///
169/// The three differ in where a delayed copy of a delivery goes. A [`CoreSubject`] is its own
170/// destination, so the framework publishes the copy back to it. A [`CoreWildcard`] names many
171/// subjects and none in particular, so its mount site names one with `out_retry(..).to(..)`. A
172/// [`JetStreamSubject`] needs no copy: the server holds the message for the delay.
173///
174/// # Examples
175///
176/// ```
177/// # mod demo {
178/// use std::time::Duration;
179///
180/// use ruststream_nats::prelude::*;
181/// use serde::Deserialize;
182///
183/// #[derive(Deserialize)]
184/// struct Payment {
185///     id: u64,
186///     settled: bool,
187/// }
188///
189/// #[subscriber("payments.settled")]
190/// async fn reconcile(payment: &Payment) -> HandlerOutcome {
191///     if payment.settled {
192///         return HandlerOutcome::ack();
193///     }
194///     HandlerOutcome::retry_after(Duration::from_secs(30))
195/// }
196///
197/// #[subscriber(CoreWildcard::new("payments.*"))]
198/// async fn audit(payment: &Payment) -> HandlerOutcome {
199///     tracing::warn!(payment.id, "audit pending");
200///     HandlerOutcome::retry_after(Duration::from_secs(30))
201/// }
202///
203/// #[subscriber(JetStreamSubject::new("payments.stored", "PAYMENTS").durable("reconciler"))]
204/// async fn store(payment: &Payment) -> HandlerOutcome {
205///     tracing::warn!(payment.id, "store pending");
206///     HandlerOutcome::retry_after(Duration::from_secs(30))
207/// }
208///
209/// #[ruststream::app]
210/// fn app() -> impl App {
211///     RustStream::new(AppInfo::new("payments", "0.1.0")).with_broker(
212///         NatsBroker::new("nats://localhost:4222"),
213///         |b| {
214///             // One subject is its own destination.
215///             b.include(reconcile)
216///                 .max_attempts(nonzero!(5u32))
217///                 .dead_letter("payments.dead");
218///
219///             // A pattern is not, so the mount site names where a copy lands.
220///             b.include(audit)
221///                 .max_attempts(nonzero!(5u32))
222///                 .dead_letter("dead.payments")
223///                 .out_retry(Publish)
224///                 .to("payments.settled");
225///
226///             // The server holds the message, so nothing is published at all.
227///             b.include(store)
228///                 .max_attempts(nonzero!(5u32))
229///                 .dead_letter("payments.dead");
230///         },
231///     )
232/// }
233/// # }
234/// # fn main() {}
235/// ```
236#[diagnostic::on_unimplemented(
237    message = "`{Self}` does not describe a NATS subscription",
238    label = "not a subscription descriptor",
239    note = "use `CoreSubject::new(subject)` for one Core NATS subject, \
240            `CoreWildcard::new(pattern)` for a Core NATS pattern, or \
241            `JetStreamSubject::new(subject, stream)` for a JetStream pull consumer"
242)]
243pub trait NatsSubscription: Sealed {
244    /// The subject pattern this subscription receives messages on.
245    fn subject(&self) -> &str;
246
247    /// The one thing about a subscription the types cannot settle: the subject is a string the
248    /// caller supplies, often from configuration, and a descriptor constructor is the expression
249    /// a `#[subscriber(..)]` attribute writes, so it cannot hand back a `Result`. Every
250    /// `subscribe` implementation calls this before it does any work.
251    ///
252    /// # Errors
253    ///
254    /// Returns [`NatsError::InvalidOptions`] when the subject is empty.
255    fn ensure_subject(&self) -> Result<(), NatsError> {
256        if self.subject().is_empty() {
257            return Err(NatsError::InvalidOptions(
258                "subject must be non-empty".into(),
259            ));
260        }
261        Ok(())
262    }
263}
264
265/// Whether `subject` carries a NATS wildcard: `*` for one token, `>` for the rest of them.
266///
267/// A wildcard is matched on delivery. A publish takes it as a literal token (a pedantic connection
268/// refuses it), so a copy published to a pattern would not reach the subject its delivery came
269/// from, and this crate treats a pattern as a subscription and never as a destination.
270fn has_wildcard(subject: &str) -> bool {
271    subject.split('.').any(|token| token == "*" || token == ">")
272}
273
274/// The subject as a publish destination.
275///
276/// # Errors
277///
278/// Returns [`NatsError::InvalidOptions`] when the subject is empty or carries a wildcard.
279pub(crate) fn publish_destination(subject: &str) -> Result<RedeliveryAddress, NatsError> {
280    if subject.is_empty() {
281        return Err(NatsError::InvalidOptions(
282            "subject must be non-empty".into(),
283        ));
284    }
285    if has_wildcard(subject) {
286        return Err(NatsError::InvalidOptions(format!(
287            "`{subject}` is a subscription pattern, not a subject a delayed copy can be \
288             published to. Subscribe to a pattern with `CoreWildcard`, which leaves the mount \
289             site to name where a retry copy goes"
290        )));
291    }
292    Ok(RedeliveryAddress::new(subject.to_owned()))
293}
294
295/// One subject subscribed to over Core NATS.
296///
297/// Core NATS delivers to whoever is subscribed at that moment and stores nothing, so the only
298/// setting it has is the queue group that load-balances a subject across several subscribers.
299/// Reading a stream instead is [`JetStreamSubject`]; reading many subjects at once is
300/// [`CoreWildcard`].
301///
302/// The subject is one destination, so a delayed redelivery needs nothing from the mount site: the
303/// framework publishes its copy back to this very subject.
304///
305/// # Examples
306///
307/// ```
308/// # mod demo {
309/// use ruststream_nats::prelude::*;
310/// use serde::Deserialize;
311///
312/// #[derive(Deserialize)]
313/// struct Order {
314///     id: u64,
315/// }
316///
317/// // Every replica joins `workers`, so each order reaches one of them.
318/// #[subscriber(CoreSubject::new("orders.created").queue_group("workers"))]
319/// async fn fulfil(order: &Order) -> HandlerOutcome {
320///     tracing::info!(order.id, "fulfilled");
321///     HandlerOutcome::ack()
322/// }
323///
324/// #[ruststream::app]
325/// fn app() -> impl App {
326///     RustStream::new(AppInfo::new("fulfillment", "0.1.0")).with_broker(
327///         NatsBroker::new("nats://localhost:4222"),
328///         |b| {
329///             b.include(fulfil);
330///         },
331///     )
332/// }
333/// # }
334/// # fn main() {}
335/// ```
336///
337/// A `JetStream` setting is not a method here, so asking for one does not compile:
338///
339/// ```compile_fail
340/// use ruststream_nats::CoreSubject;
341///
342/// // `durable` names a JetStream consumer, and Core NATS has none.
343/// let bad = CoreSubject::new("orders.created").durable("worker-1");
344/// ```
345#[derive(Debug, Clone, PartialEq, Eq)]
346#[must_use]
347pub struct CoreSubject {
348    subject: String,
349    queue_group: Option<String>,
350}
351
352impl CoreSubject {
353    /// Subscribes to `subject` over Core NATS.
354    pub fn new(subject: impl Into<String>) -> Self {
355        Self {
356            subject: subject.into(),
357            queue_group: None,
358        }
359    }
360
361    /// Load-balances the subject across a queue group: the server hands each message to one
362    /// member instead of all of them.
363    pub fn queue_group(mut self, name: impl Into<String>) -> Self {
364        self.queue_group = Some(name.into());
365        self
366    }
367
368    /// The queue group this subscription joins, if any.
369    #[cfg(feature = "asyncapi")]
370    pub(crate) fn group(&self) -> Option<&str> {
371        self.queue_group.as_deref()
372    }
373}
374
375impl Sealed for CoreSubject {
376    fn plan(&self) -> SubscriptionPlan<'_> {
377        SubscriptionPlan::Core {
378            queue_group: self.queue_group.as_deref(),
379        }
380    }
381}
382
383/// A pattern subscribed to over Core NATS: `orders.*`, `orders.>`, `>`.
384///
385/// The same subscription as [`CoreSubject`], on many subjects instead of one. That is the whole
386/// difference between the two types, and it is the difference that matters to a retry: a pattern
387/// names many subjects and none of them in particular, so this subscription cannot say where a
388/// copy of a delivery would reach it again. The mount site says it instead, with
389/// `.out_retry(Publish).to("orders.created")` or with a transform that reads the delivery and
390/// names its own subject; a registration that says neither refuses to start.
391///
392/// # Examples
393///
394/// ```
395/// # mod demo {
396/// use std::time::Duration;
397///
398/// use ruststream_nats::prelude::*;
399/// use serde::Deserialize;
400///
401/// #[derive(Deserialize)]
402/// struct Order {
403///     id: u64,
404///     paid: bool,
405/// }
406///
407/// #[subscriber(CoreWildcard::new("orders.*").queue_group("auditors"))]
408/// async fn audit(order: &Order) -> HandlerOutcome {
409///     if order.paid {
410///         return HandlerOutcome::ack();
411///     }
412///     HandlerOutcome::retry_after(Duration::from_secs(30))
413/// }
414///
415/// #[ruststream::app]
416/// fn app() -> impl App {
417///     RustStream::new(AppInfo::new("audit", "0.1.0")).with_broker(
418///         NatsBroker::new("nats://localhost:4222"),
419///         |b| {
420///             // A pattern is no destination, so the mount site names where a delayed copy goes.
421///             b.include(audit)
422///                 .max_attempts(nonzero!(5u32))
423///                 .dead_letter("orders.dead")
424///                 .out_retry(Publish)
425///                 .to("orders.audit");
426///         },
427///     )
428/// }
429/// # }
430/// # fn main() {}
431/// ```
432#[derive(Debug, Clone, PartialEq, Eq)]
433#[must_use]
434pub struct CoreWildcard {
435    subject: String,
436    queue_group: Option<String>,
437}
438
439impl CoreWildcard {
440    /// Subscribes to the pattern `subject` over Core NATS.
441    pub fn new(subject: impl Into<String>) -> Self {
442        Self {
443            subject: subject.into(),
444            queue_group: None,
445        }
446    }
447
448    /// Load-balances the pattern across a queue group, exactly as
449    /// [`CoreSubject::queue_group`] does for one subject.
450    pub fn queue_group(mut self, name: impl Into<String>) -> Self {
451        self.queue_group = Some(name.into());
452        self
453    }
454
455    /// The queue group this subscription joins, if any.
456    #[cfg(feature = "asyncapi")]
457    pub(crate) fn group(&self) -> Option<&str> {
458        self.queue_group.as_deref()
459    }
460}
461
462impl Sealed for CoreWildcard {
463    fn plan(&self) -> SubscriptionPlan<'_> {
464        SubscriptionPlan::Core {
465            queue_group: self.queue_group.as_deref(),
466        }
467    }
468}
469
470impl NatsSubscription for CoreWildcard {
471    fn subject(&self) -> &str {
472        &self.subject
473    }
474}
475
476impl NatsSubscription for CoreSubject {
477    fn subject(&self) -> &str {
478        &self.subject
479    }
480
481    /// A wildcard belongs to [`CoreWildcard`], so it is refused here rather than opened as a
482    /// subscription this type promises to address.
483    ///
484    /// The subject is a string the caller supplies, often from configuration, so the two forms
485    /// cannot be told apart while the crate compiles. The subscription refuses to open instead,
486    /// which is a service that does not start rather than one that starts and loses a delayed
487    /// message.
488    fn ensure_subject(&self) -> Result<(), NatsError> {
489        publish_destination(self.subject()).map(|_| ())
490    }
491}
492
493/// A subject read through a `JetStream` pull consumer on one stream.
494///
495/// The consumer is created when the subscription opens. Name it with [`durable`](Self::durable)
496/// and the server keeps its position across restarts; leave the name out and the consumer is
497/// ephemeral, discarded when the subscriber drops.
498///
499/// # Examples
500///
501/// ```
502/// # mod demo {
503/// use ruststream_nats::prelude::*;
504/// use serde::Deserialize;
505///
506/// #[derive(Deserialize)]
507/// struct Order {
508///     id: u64,
509/// }
510///
511/// // The durable keeps its position across restarts, and every replica shares it.
512/// #[subscriber(
513///     JetStreamSubject::new("orders.*", "ORDERS")
514///         .durable("billing")
515///         .ack_wait(NonZeroDuration::from_secs(nonzero!(30)))
516/// )]
517/// async fn bill(order: &Order) -> HandlerOutcome {
518///     tracing::info!(order.id, "billed");
519///     HandlerOutcome::ack()
520/// }
521///
522/// #[ruststream::app]
523/// fn app() -> impl App {
524///     RustStream::new(AppInfo::new("billing", "0.1.0")).with_broker(
525///         NatsBroker::new("nats://localhost:4222"),
526///         |b| {
527///             b.include(bill);
528///         },
529///     )
530/// }
531/// # }
532/// # fn main() {}
533/// ```
534///
535/// A queue group is Core NATS only, so it is not a method here:
536///
537/// ```compile_fail
538/// use ruststream_nats::JetStreamSubject;
539///
540/// let bad = JetStreamSubject::new("orders.*", "ORDERS").queue_group("workers");
541/// ```
542#[derive(Debug, Clone)]
543#[must_use]
544pub struct JetStreamSubject {
545    subject: String,
546    stream: String,
547    durable: Option<String>,
548    filter_subject: Option<String>,
549    ack_wait: Option<NonZeroDuration>,
550    max_ack_pending: Option<i64>,
551    deliver_policy: Option<DeliverPolicy>,
552    pull_expires: Option<NonZeroDuration>,
553}
554
555impl JetStreamSubject {
556    /// Reads `subject` from the stream named `stream`.
557    ///
558    /// `subject` is the pattern the subscription reports and the default consumer filter;
559    /// `stream` is the `JetStream` stream that stores it.
560    pub fn new(subject: impl Into<String>, stream: impl Into<String>) -> Self {
561        Self {
562            subject: subject.into(),
563            stream: stream.into(),
564            durable: None,
565            filter_subject: None,
566            ack_wait: None,
567            max_ack_pending: None,
568            deliver_policy: None,
569            pull_expires: None,
570        }
571    }
572
573    /// Names the consumer, so the server keeps its position across restarts.
574    pub fn durable(mut self, name: impl Into<String>) -> Self {
575        self.durable = Some(name.into());
576        self
577    }
578
579    /// Narrows which subjects of the stream this consumer reads. Defaults to the subject the
580    /// constructor named.
581    pub fn filter_subject(mut self, subject: impl Into<String>) -> Self {
582        self.filter_subject = Some(subject.into());
583        self
584    }
585
586    /// Per-message acknowledgement timeout. After this window an unacked delivery is redelivered.
587    /// Defaults to 30 seconds.
588    pub const fn ack_wait(mut self, ack_wait: NonZeroDuration) -> Self {
589        self.ack_wait = Some(ack_wait);
590        self
591    }
592
593    /// Soft cap on in-flight unacked deliveries. Defaults to 1024.
594    pub const fn max_ack_pending(mut self, max: i64) -> Self {
595        self.max_ack_pending = Some(max);
596        self
597    }
598
599    /// Where a newly created consumer starts reading. Defaults to `DeliverPolicy::All`.
600    pub const fn deliver_policy(mut self, policy: DeliverPolicy) -> Self {
601        self.deliver_policy = Some(policy);
602        self
603    }
604
605    /// How long one fetch waits before delivering a partial (or retrying an empty) batch.
606    /// Defaults to 5 seconds. Has no effect on the per-message
607    /// [`Subscriber::stream`](ruststream::Subscriber::stream) path.
608    ///
609    /// How many messages a batch carries is not set here: the batch size travels with the
610    /// registration (`b.include(handler.batch(nonzero!(6)))`) and reaches the fetch as the
611    /// argument of [`BatchSubscriber::batches`](ruststream::BatchSubscriber::batches).
612    pub const fn pull_expires(mut self, expires: NonZeroDuration) -> Self {
613        self.pull_expires = Some(expires);
614        self
615    }
616
617    /// The subjects the consumer actually reads: the filter where one is set, the subject
618    /// otherwise.
619    pub(crate) fn consumer_filter(&self) -> &str {
620        self.filter_subject.as_deref().unwrap_or(&self.subject)
621    }
622
623    /// What this consumer is, in `JetStream`'s own words.
624    ///
625    /// The `nats` binding has one field and it belongs to a Core NATS queue group, so everything
626    /// a stream consumer is configured with goes under an extension of this crate's own. Every
627    /// value is the descriptor's; nothing here waits for a connection.
628    #[cfg(feature = "asyncapi")]
629    fn consumer_bindings(&self) -> Bindings {
630        let body = JetStreamChannel {
631            stream: &self.stream,
632            durable: self.durable.as_deref(),
633            filter_subject: self.consumer_filter(),
634            ack_wait_seconds: self
635                .ack_wait
636                .map_or(DEFAULT_ACK_WAIT, NonZeroDuration::get)
637                .as_secs_f64(),
638            max_ack_pending: self.max_ack_pending.unwrap_or(DEFAULT_MAX_ACK_PENDING),
639            deliver_policy: deliver_policy_name(self.deliver_policy.unwrap_or(DeliverPolicy::All)),
640        };
641        Binding::extension(JETSTREAM_EXTENSION, &body)
642            .map(|binding| Bindings::new().with(binding))
643            .unwrap_or_default()
644    }
645}
646
647/// The extension key the `JetStream` consumer settings travel under.
648///
649/// The specification's `nats` binding declares its channel object empty, and the protocol keys are
650/// a closed list, so a stream, a durable name and an ack window have no lawful binding field. An
651/// `x-` extension sits at the same level and carries no `bindingVersion`.
652#[cfg(feature = "asyncapi")]
653pub(crate) const JETSTREAM_EXTENSION: &str = "x-ruststream-jetstream";
654
655/// The whole `nats` binding, version 0.1.0: the queue group of a Core NATS subscription, on the
656/// `receive` operation. A subscription outside a group contributes nothing rather than an empty
657/// object.
658#[cfg(feature = "asyncapi")]
659fn core_operation_bindings(queue_group: Option<&str>) -> Bindings {
660    queue_group
661        .map(|queue| NatsOperation { queue })
662        .and_then(|body| Binding::new("nats", "0.1.0", &body).ok())
663        .map(|binding| Bindings::new().with(binding))
664        .unwrap_or_default()
665}
666
667/// The name `JetStream` gives a delivery policy on the wire.
668#[cfg(feature = "asyncapi")]
669fn deliver_policy_name(policy: DeliverPolicy) -> &'static str {
670    match policy {
671        DeliverPolicy::All => "all",
672        DeliverPolicy::Last => "last",
673        DeliverPolicy::New => "new",
674        DeliverPolicy::ByStartSequence { .. } => "by_start_sequence",
675        DeliverPolicy::ByStartTime { .. } => "by_start_time",
676        DeliverPolicy::LastPerSubject => "last_per_subject",
677    }
678}
679
680/// The queue group, which is the whole `nats` binding.
681#[cfg(feature = "asyncapi")]
682#[derive(Serialize)]
683struct NatsOperation<'a> {
684    queue: &'a str,
685}
686
687/// What a `JetStream` consumer is configured with, under this crate's extension key.
688#[cfg(feature = "asyncapi")]
689#[derive(Serialize)]
690#[serde(rename_all = "camelCase")]
691struct JetStreamChannel<'a> {
692    stream: &'a str,
693    #[serde(skip_serializing_if = "Option::is_none")]
694    durable: Option<&'a str>,
695    filter_subject: &'a str,
696    ack_wait_seconds: f64,
697    max_ack_pending: i64,
698    deliver_policy: &'static str,
699}
700
701impl Sealed for JetStreamSubject {
702    fn plan(&self) -> SubscriptionPlan<'_> {
703        SubscriptionPlan::JetStream {
704            stream: &self.stream,
705            durable: self.durable.as_deref(),
706            filter_subject: self.filter_subject.as_deref().unwrap_or(&self.subject),
707            ack_wait: self.ack_wait.map_or(DEFAULT_ACK_WAIT, NonZeroDuration::get),
708            max_ack_pending: self.max_ack_pending.unwrap_or(DEFAULT_MAX_ACK_PENDING),
709            deliver_policy: self.deliver_policy.unwrap_or(DeliverPolicy::All),
710            pull_expires: self
711                .pull_expires
712                .map_or(DEFAULT_PULL_EXPIRES, NonZeroDuration::get),
713        }
714    }
715}
716
717impl NatsSubscription for JetStreamSubject {
718    fn subject(&self) -> &str {
719        &self.subject
720    }
721}
722
723/// A descriptor is already a source, so the macro-free constructor takes it as it stands:
724/// `subscriber(JetStreamSubject::new("orders.*", "ORDERS"), body)` names the same subscription
725/// the `#[subscriber(..)]` attribute does.
726///
727/// # Examples
728///
729/// ```
730/// # mod demo {
731/// use ruststream::schemars::JsonSchema;
732/// use ruststream_nats::prelude::*;
733/// use serde::Deserialize;
734///
735/// #[derive(Deserialize, JsonSchema)]
736/// # #[schemars(crate = "ruststream::schemars")]
737/// struct Order {
738///     id: u64,
739/// }
740///
741/// struct Audit;
742///
743/// impl Handle<Order> for Audit {
744///     async fn handle(
745///         &self,
746///         order: &Order,
747///         _outs: &(),
748///         _ctx: &mut Context<'_>,
749///     ) -> Result<(), HandlerOutcome> {
750///         tracing::info!(order.id, "audited");
751///         Ok(())
752///     }
753/// }
754///
755/// #[ruststream::app]
756/// fn app() -> impl App {
757///     RustStream::new(AppInfo::new("audit", "0.1.0")).with_broker(
758///         NatsBroker::new("nats://localhost:4222"),
759///         |b| {
760///             let audited = CoreSubject::new("orders.created").queue_group("auditors");
761///             b.include(subscriber(audited, Audit).build());
762///         },
763///     )
764/// }
765/// # }
766/// # fn main() {}
767/// ```
768impl IntoSource for CoreSubject {
769    type Source = Self;
770
771    fn into_source(self) -> Self {
772        self
773    }
774}
775
776impl IntoSource for JetStreamSubject {
777    type Source = Self;
778
779    fn into_source(self) -> Self {
780        self
781    }
782}
783
784impl IntoSource for CoreWildcard {
785    type Source = Self;
786
787    fn into_source(self) -> Self {
788        self
789    }
790}
791
792impl SubscriptionSource<ConnectedNatsBroker> for CoreSubject {
793    type Subscriber = NatsSubscriber;
794    // One subject, published to and subscribed to under one name, so the framework's deferred
795    // copy needs nothing from the mount site.
796    type Copies = AddressedCopies;
797
798    fn name(&self) -> &str {
799        NatsSubscription::subject(self)
800    }
801
802    async fn subscribe(
803        self,
804        connected: &ConnectedNatsBroker,
805    ) -> Result<Self::Subscriber, NatsError> {
806        connected.subscribe_with(self).await
807    }
808
809    #[cfg(feature = "asyncapi")]
810    fn operation_bindings(&self) -> Bindings {
811        core_operation_bindings(self.group())
812    }
813}
814
815/// The subject itself, so the answer needs no connection and the future is ready.
816impl RedeliveryAddressed<ConnectedNatsBroker> for CoreSubject {
817    fn redelivery_address(
818        &self,
819        _connected: &ConnectedNatsBroker,
820    ) -> impl Future<Output = Result<RedeliveryAddress, NatsError>> + Send {
821        ready(publish_destination(NatsSubscription::subject(self)))
822    }
823}
824
825impl SubscriptionSource<ConnectedNatsBroker> for CoreWildcard {
826    type Subscriber = NatsSubscriber;
827    // A pattern reads many subjects and names none of them.
828    type Copies = NamedCopies;
829
830    fn name(&self) -> &str {
831        NatsSubscription::subject(self)
832    }
833
834    async fn subscribe(
835        self,
836        connected: &ConnectedNatsBroker,
837    ) -> Result<Self::Subscriber, NatsError> {
838        connected.subscribe_with(self).await
839    }
840
841    #[cfg(feature = "asyncapi")]
842    fn operation_bindings(&self) -> Bindings {
843        core_operation_bindings(self.group())
844    }
845}
846
847impl SubscriptionSource<ConnectedNatsBroker> for JetStreamSubject {
848    type Subscriber = NatsSubscriber;
849    // The consumer reads its filter subject, and a publish there reaches it.
850    type Copies = AddressedCopies;
851
852    fn name(&self) -> &str {
853        NatsSubscription::subject(self)
854    }
855
856    async fn subscribe(
857        self,
858        connected: &ConnectedNatsBroker,
859    ) -> Result<Self::Subscriber, NatsError> {
860        connected.subscribe_with(self).await
861    }
862
863    #[cfg(feature = "asyncapi")]
864    fn channel_bindings(&self) -> Bindings {
865        self.consumer_bindings()
866    }
867}
868
869/// The consumer's filter subject, which defaults to the subject the descriptor names.
870///
871/// A `JetStream` consumer holds a delivery itself and takes the delay in its negative
872/// acknowledgement, so the framework publishes no copy here and this address is what the
873/// conformance suite checks rather than what a running service uses. A consumer filtering a
874/// pattern reports that pattern, which is not a publish destination; nothing publishes to it, and
875/// a `.to(name)` at the mount site overrides it.
876impl RedeliveryAddressed<ConnectedNatsBroker> for JetStreamSubject {
877    fn redelivery_address(
878        &self,
879        _connected: &ConnectedNatsBroker,
880    ) -> impl Future<Output = Result<RedeliveryAddress, NatsError>> + Send {
881        ready(Ok(RedeliveryAddress::new(
882            self.consumer_filter().to_owned(),
883        )))
884    }
885}
886
887#[cfg(test)]
888mod tests {
889    use ruststream::nonzero;
890
891    use super::*;
892
893    #[test]
894    fn a_plain_subscription_is_core_without_a_queue_group() {
895        let opts = CoreSubject::new("orders.created");
896        opts.ensure_subject().expect("core ok");
897        assert!(matches!(
898            opts.plan(),
899            SubscriptionPlan::Core { queue_group: None }
900        ));
901    }
902
903    #[test]
904    fn a_queue_group_reaches_the_broker() {
905        let opts = CoreSubject::new("orders.created").queue_group("workers");
906        assert!(matches!(
907            opts.plan(),
908            SubscriptionPlan::Core {
909                queue_group: Some("workers")
910            }
911        ));
912    }
913
914    #[test]
915    fn jetstream_defaults_are_resolved_in_the_plan() {
916        let consumer = JetStreamSubject::new("orders.*", "ORDERS");
917        let SubscriptionPlan::JetStream {
918            stream,
919            durable,
920            filter_subject,
921            ack_wait,
922            max_ack_pending,
923            pull_expires,
924            ..
925        } = consumer.plan()
926        else {
927            panic!("a jetstream descriptor must plan a jetstream subscription");
928        };
929        assert_eq!(stream, "ORDERS");
930        assert_eq!(durable, None);
931        // The filter defaults to the subject, which only the descriptor knows.
932        assert_eq!(filter_subject, "orders.*");
933        assert_eq!(ack_wait, DEFAULT_ACK_WAIT);
934        assert_eq!(max_ack_pending, DEFAULT_MAX_ACK_PENDING);
935        assert_eq!(pull_expires, DEFAULT_PULL_EXPIRES);
936    }
937
938    // What a reported address promises is that a publish there arrives, so a pattern the server
939    // would refuse a publish to has no business being one. `CoreSubject` carries the subject that
940    // is a destination, and refuses to open on anything else.
941    #[test]
942    fn only_a_concrete_subject_is_a_redelivery_address() {
943        assert_eq!(
944            publish_destination("orders.created").expect("a literal subject is a destination"),
945            RedeliveryAddress::new("orders.created")
946        );
947        for pattern in ["orders.*", "orders.>", ">", ""] {
948            assert!(
949                CoreSubject::new(pattern).ensure_subject().is_err(),
950                "`{pattern}` is not a destination, so CoreSubject must refuse it"
951            );
952            assert!(publish_destination(pattern).is_err());
953        }
954    }
955
956    // A pattern is the other type's business, and it opens on exactly what Core NATS accepts.
957    #[test]
958    fn a_pattern_opens_as_a_wildcard_subscription() {
959        for pattern in ["orders.*", "orders.>", ">", "orders.created"] {
960            CoreWildcard::new(pattern)
961                .ensure_subject()
962                .expect("a pattern is what this descriptor is for");
963        }
964        assert!(CoreWildcard::new("").ensure_subject().is_err());
965    }
966
967    // A JetStream consumer reads its filter, so a narrower filter is the address, not the subject
968    // the descriptor was constructed with.
969    #[test]
970    fn a_jetstream_consumer_reports_the_subject_it_actually_reads() {
971        assert_eq!(
972            JetStreamSubject::new("orders.*", "ORDERS")
973                .filter_subject("orders.created")
974                .consumer_filter(),
975            "orders.created"
976        );
977        assert_eq!(
978            JetStreamSubject::new("orders.*", "ORDERS").consumer_filter(),
979            "orders.*"
980        );
981    }
982
983    #[test]
984    fn jetstream_settings_reach_the_plan() {
985        let consumer = JetStreamSubject::new("orders.*", "ORDERS")
986            .durable("worker")
987            .filter_subject("orders.created")
988            .ack_wait(NonZeroDuration::from_secs(nonzero!(5)))
989            .max_ack_pending(64)
990            .deliver_policy(DeliverPolicy::New)
991            .pull_expires(NonZeroDuration::from_millis(nonzero!(250)));
992        let SubscriptionPlan::JetStream {
993            durable,
994            filter_subject,
995            ack_wait,
996            max_ack_pending,
997            pull_expires,
998            ..
999        } = consumer.plan()
1000        else {
1001            panic!("a jetstream descriptor must plan a jetstream subscription");
1002        };
1003        assert_eq!(durable, Some("worker"));
1004        assert_eq!(filter_subject, "orders.created");
1005        assert_eq!(ack_wait, Duration::from_secs(5));
1006        assert_eq!(max_ack_pending, 64);
1007        assert_eq!(pull_expires, Duration::from_millis(250));
1008    }
1009
1010    #[test]
1011    fn an_empty_subject_is_rejected_on_both_descriptors() {
1012        let core = CoreSubject::new("").ensure_subject().unwrap_err();
1013        assert!(matches!(core, NatsError::InvalidOptions(msg) if msg.contains("subject")));
1014        let js = JetStreamSubject::new("", "ORDERS")
1015            .ensure_subject()
1016            .unwrap_err();
1017        assert!(matches!(js, NatsError::InvalidOptions(msg) if msg.contains("subject")));
1018    }
1019
1020    #[test]
1021    fn a_zero_duration_has_no_non_zero_form() {
1022        assert!(NonZeroDuration::new(Duration::ZERO).is_none());
1023        assert_eq!(
1024            NonZeroDuration::new(Duration::from_millis(1)).map(NonZeroDuration::get),
1025            Some(Duration::from_millis(1))
1026        );
1027    }
1028}