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}