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}