Skip to main content

ruststream/runtime/handle/
reply.rs

1//! The reply cells of the matrix: a sealed definition whose body declared a reply mounts
2//! through the publishing machinery, with the policy the chain attached (or the broker's
3//! default) committed right at `include`.
4
5use std::any::type_name;
6
7use crate::runtime::batch::BatchResult;
8use crate::runtime::batch_publishing::{BatchPublishingCall, BatchPublishingDef};
9use crate::runtime::context::Context;
10use crate::runtime::handler::HandlerOutcome;
11use crate::runtime::metadata::{OutgoingKind, OutgoingMessageMetadata};
12use crate::runtime::publishing::{PublishingCall, PublishingDef};
13use crate::runtime::router::{IncludeDef, forms};
14use crate::{CallerName, FixedName, Name, OutgoingDestination, Unnamed};
15
16use super::Handle;
17use super::axis::{
18    Axis, AxisDocs, Batch, BatchPair, BatchedAxis, Deserialized, Input, Message, Solo, SoloAxis,
19    SoloDeserialized, SoloPair,
20};
21use super::docs::DocState;
22use super::eager::construct;
23use super::value::{
24    DeclaredDest, EncodedReply, HandleValue, NamedDest, ReplyValue, ResolvedDest, Sealed,
25    SerializedReply,
26};
27use super::verdict::{Batched, OneByOne};
28// The self-serialized vocabulary lives with the publish builder (the general wire seam serves
29// every typed surface); re-exported here so the reply seam keeps reading as one module.
30pub use crate::runtime::publish::Serialized;
31
32/// The shape and wire of a reply value: what payload it publishes, what typed header contract
33/// rides it, and whether the framework's codec serializes it.
34///
35/// Implemented for every `serde::Serialize` type and every [`Message`] pair of them (the
36/// [`EncodedReply`] wire), and per-type for [`Serialized`] replies (the [`SerializedReply`]
37/// wire) - `#[derive(Serialized)]` writes that impl, or see [`Serialized`] for the hand-written
38/// form.
39#[diagnostic::on_unimplemented(
40    message = "`{Self}` is not a reply value",
41    note = "a reply is a `serde::Serialize` value (the reply codec encodes it), a \
42            `Message<Headers, Payload>` pair of them, or a `#[derive(Serialized)]` type (its \
43            bytes leave as they are)"
44)]
45pub trait ReplyShape: Send + Sync {
46    /// The published payload type (the pair's body, or the reply itself).
47    #[doc(hidden)]
48    type Body: Send + Sync;
49    /// The typed header contract riding the reply (`()` when none does).
50    #[doc(hidden)]
51    type Headers;
52    /// The reply's wire: [`EncodedReply`] or [`SerializedReply`].
53    type Wire;
54}
55
56impl<R: serde::Serialize + Send + Sync> ReplyShape for R {
57    type Body = R;
58    type Headers = ();
59    type Wire = EncodedReply;
60}
61
62impl<H, P> ReplyShape for Message<H, P>
63where
64    H: serde::Serialize + Send + Sync,
65    P: serde::Serialize + Send + Sync,
66{
67    type Body = P;
68    type Headers = H;
69    type Wire = EncodedReply;
70}
71
72/// The reply's headers schema, produced only where headers actually ride the reply (a unit
73/// contract reports nothing rather than the unit type's schema).
74#[doc(hidden)]
75pub trait ReplyHeadersSchema<Doc>: ReplyShape {
76    fn headers_schema() -> Option<String>;
77}
78
79impl<R: serde::Serialize + Send + Sync, Doc> ReplyHeadersSchema<Doc> for R {
80    fn headers_schema() -> Option<String> {
81        None
82    }
83}
84
85impl<H, P, Doc> ReplyHeadersSchema<Doc> for Message<H, P>
86where
87    H: serde::Serialize + Send + Sync,
88    P: serde::Serialize + Send + Sync,
89    Doc: DocState<H>,
90{
91    fn headers_schema() -> Option<String> {
92        Doc::schema()
93    }
94}
95
96/// What one documentation state reports for one reply wire: the encoded wire reports the
97/// reply's schemas, the serialized wire has no serde model to report. Machinery behind the
98/// generated document; keyed by the wire marker so a `Serialized` reply mounts documented
99/// without a `JsonSchema` obligation.
100#[doc(hidden)]
101pub trait WireDocs<R: ReplyShape + ?Sized, Doc> {
102    /// True on the serialized wire: the missing payload schema is by design there.
103    const SERIALIZED: bool;
104
105    /// The serialized JSON Schema of the reply payload.
106    fn payload_schema() -> Option<String>;
107
108    /// The serialized JSON Schema of the typed header contract riding the reply.
109    fn headers_schema() -> Option<String>;
110}
111
112impl<R, Doc> WireDocs<R, Doc> for EncodedReply
113where
114    R: ReplyShape + ReplyHeadersSchema<Doc>,
115    Doc: DocState<R::Body>,
116{
117    const SERIALIZED: bool = false;
118
119    fn payload_schema() -> Option<String> {
120        <Doc as DocState<R::Body>>::schema()
121    }
122
123    fn headers_schema() -> Option<String> {
124        <R as ReplyHeadersSchema<Doc>>::headers_schema()
125    }
126}
127
128impl<R: ReplyShape + ?Sized, Doc> WireDocs<R, Doc> for SerializedReply {
129    const SERIALIZED: bool = true;
130
131    fn payload_schema() -> Option<String> {
132        None
133    }
134
135    fn headers_schema() -> Option<String> {
136        None
137    }
138}
139
140/// Resolves one declared destination form against the mount site's name: a fixed name is the
141/// destination and the mount-site name does not apply, a type declaring none takes it.
142///
143/// [`NameTemplate`](crate::NameTemplate) has no impl on purpose: a reply is published by the
144/// runtime, which has nothing to bind the placeholders with.
145#[doc(hidden)]
146pub trait ResolveDestination<Form> {
147    /// The destination, given the name the mount site supplied.
148    fn resolve(default: &str) -> &str;
149}
150
151impl<R: OutgoingDestination<Form = FixedName>> ResolveDestination<FixedName> for R {
152    fn resolve(_default: &str) -> &str {
153        <R as OutgoingDestination>::DESTINATION
154    }
155}
156
157impl<R: OutgoingDestination<Form = CallerName>> ResolveDestination<CallerName> for R {
158    fn resolve(default: &str) -> &str {
159        default
160    }
161}
162
163/// Where a reply type is published, resolved from its payload's own declaration and the
164/// mount-site name.
165#[doc(hidden)]
166#[diagnostic::on_unimplemented(
167    message = "`{Self}` does not declare where it is published",
168    note = "derive `Outgoing` on the reply type: `#[outgoing(name = \"orders.done\")]` fixes the \
169            destination, and a derive without a name is published where the mount site says \
170            (`publish(\"..\")` on the attribute, `.to(\"..\")` on the chain). A name template has \
171            no placeholders to bind on the reply path."
172)]
173pub trait ReplyDestination {
174    /// The destination, given the name the mount site supplied.
175    fn destination(default: &str) -> &str;
176}
177
178// The two nested obligations are the machinery of the resolution, not the user's mistake: the
179// trait's own note names the derive, so the impl stays out of the error.
180#[diagnostic::do_not_recommend]
181impl<R> ReplyDestination for R
182where
183    R: ReplyShape<Body: OutgoingDestination>,
184    R::Body: ResolveDestination<<R::Body as OutgoingDestination>::Form>,
185{
186    fn destination(default: &str) -> &str {
187        <R::Body as ResolveDestination<<R::Body as OutgoingDestination>::Form>>::resolve(default)
188    }
189}
190
191/// The reply type a definition produces, as a mount chain can read it before the registration
192/// commits.
193///
194/// A definition names its reply type the moment it exists - the handler's return type does not
195/// depend on the policies a chain is still binding - so a step that has to know the reply, like a
196/// `.transform(..)` mounting a transform that names the destination, reads it here. Implemented by
197/// `#[subscriber]` next to the definition and by the value path's sealed reply definition; you
198/// never name it.
199#[doc(hidden)]
200pub trait DeclaresReply {
201    /// The reply type, as [`ReplyShape`] sees it.
202    type Reply;
203
204    /// The broker's typed per-delivery context the handler reads, which is what a transform on
205    /// this reply sees through its [`PublishContext`](crate::runtime::PublishContext).
206    type Context;
207}
208
209impl<A, R, O, C, H, Doc, Dest> DeclaresReply
210    for Sealed<ReplyValue<HandleValue<A, R, O, C, H, Doc>, Dest>>
211{
212    type Reply = R;
213    type Context = C;
214}
215
216/// A reply whose destination a mount chain may name per delivery.
217///
218/// A reply type that declares its own channel (`#[outgoing(name = "..")]`) is published there, and
219/// the generated document says so; letting a transform move it would put the two back out of
220/// step, which is the whole reason the destination lives on the type. So a transform that
221/// declares `Destination = Names` mounts only on a reply type that leaves the destination open -
222/// the mount site's `publish("dest")` name is then the declared fallback, and the transform names
223/// where each answer actually goes.
224#[doc(hidden)]
225#[diagnostic::on_unimplemented(
226    message = "`{Self}` declares where it is published, so a transform cannot name its destination",
227    label = "this reply's destination is its type's own",
228    note = "drop `#[outgoing(name = \"..\")]` from the reply type and name the fallback at the \
229            mount site (`publish(\"dest\")` on the attribute, `.to(\"dest\")` on the chain), or \
230            publish the reply where its declaration says and mount a transform that declares \
231            `Destination = Reads`"
232)]
233pub trait RedirectableReply {}
234
235// The nested obligation is the machinery of the resolution, not the user's mistake: the trait's
236// own note names the fix, so the impl stays out of the error.
237#[diagnostic::do_not_recommend]
238impl<R> RedirectableReply for R where R: ReplyShape<Body: OutgoingDestination<Form = CallerName>> {}
239
240/// The destination of one reply type at the mount-site name, resolved at the expansion site.
241/// Machinery behind the macro expansion; not part of the public API.
242#[doc(hidden)]
243#[must_use]
244pub fn reply_destination<R: ReplyDestination>(default: &'static str) -> &'static str {
245    R::destination(default)
246}
247
248/// The destination of one reply type that declares its own. Machinery behind the macro
249/// expansion; not part of the public API.
250#[doc(hidden)]
251#[must_use]
252pub fn declared_reply_destination<R>() -> &'static str
253where
254    R: ReplyShape<Body: OutgoingDestination<Form = FixedName>>,
255{
256    <R::Body as OutgoingDestination>::DESTINATION
257}
258
259/// Where a wired reply goes: the reply type's own declaration, or the mount-site name where the
260/// type declares none.
261#[doc(hidden)]
262pub trait ReplyDest<R>: Send + Sync {
263    /// The subject the reply publishes to.
264    fn name(&self) -> &str;
265}
266
267// No obligation on `R`: the destination was resolved where the reply type was named, so a type
268// that declares none is reported at the handler rather than at the chain that mounts it.
269impl<R> ReplyDest<R> for ResolvedDest {
270    fn name(&self) -> &str {
271        self.0
272    }
273}
274
275impl<R: ReplyDestination> ReplyDest<R> for NamedDest {
276    fn name(&self) -> &str {
277        R::destination(&self.0)
278    }
279}
280
281impl<R> ReplyDest<R> for DeclaredDest
282where
283    R: ReplyShape<Body: OutgoingDestination<Form = FixedName>>,
284{
285    fn name(&self) -> &str {
286        <R::Body as OutgoingDestination>::DESTINATION
287    }
288}
289
290/// The form tokens of one reply wire on one verdict family: the sealed value-path tokens and
291/// the attribute path's builder-producing forms, with and without slots. The serialized wire
292/// has no batch form: a batch's replies publish through the reply codec.
293#[doc(hidden)]
294#[diagnostic::on_unimplemented(
295    message = "this reply's wire does not mount on this input family",
296    note = "an encoded reply (`serde::Serialize`) mounts one-by-one and per batch; a \
297            `Serialized` (raw-byte) reply mounts one-by-one only"
298)]
299pub trait ReplyFormFor<Fam> {
300    /// The mount token of a reply definition.
301    type Form;
302    /// The mount token of a reply definition that also carries slots.
303    type SlotForm;
304}
305
306impl ReplyFormFor<OneByOne> for EncodedReply {
307    type Form = forms::Publishing;
308    type SlotForm = forms::PublishingOut;
309}
310
311impl ReplyFormFor<Batched> for EncodedReply {
312    type Form = forms::BatchPublishing;
313    type SlotForm = forms::BatchPublishingOut;
314}
315
316impl ReplyFormFor<OneByOne> for SerializedReply {
317    type Form = forms::RawReply;
318    type SlotForm = forms::RawReplyOut;
319}
320
321/// The route of one reply type on one verdict family: its wire, and the form tokens that wire
322/// selects. One-by-one the reply type routes itself; per batch the `Vec<Reply>` verdict routes
323/// by its element. Machinery behind `include` and the reply chain; never named in user code.
324#[doc(hidden)]
325pub trait ReplyRoute<Fam> {
326    /// The reply's wire marker.
327    type Wire: ReplyFormFor<Fam>;
328    /// See [`ReplyFormFor::Form`].
329    type Form;
330    /// See [`ReplyFormFor::SlotForm`].
331    type SlotForm;
332}
333
334impl<R> ReplyRoute<OneByOne> for R
335where
336    R: ReplyShape,
337    R::Wire: ReplyFormFor<OneByOne>,
338{
339    type Wire = R::Wire;
340    type Form = <R::Wire as ReplyFormFor<OneByOne>>::Form;
341    type SlotForm = <R::Wire as ReplyFormFor<OneByOne>>::SlotForm;
342}
343
344impl<R> ReplyRoute<Batched> for Vec<R>
345where
346    R: ReplyShape,
347    R::Wire: ReplyFormFor<Batched>,
348{
349    type Wire = R::Wire;
350    type Form = <R::Wire as ReplyFormFor<Batched>>::Form;
351    type SlotForm = <R::Wire as ReplyFormFor<Batched>>::SlotForm;
352}
353
354impl<A, R, C, H, Doc, Dest> IncludeDef
355    for Sealed<ReplyValue<HandleValue<A, R, (), C, H, Doc>, Dest>>
356where
357    A: Axis,
358    R: ReplyRoute<A::Family>,
359{
360    type Form = R::Form;
361}
362impl<A, R, C, H, Doc, Dest> PublishingDef
363    for Sealed<ReplyValue<HandleValue<A, R, (), C, H, Doc>, Dest>>
364where
365    A: SoloAxis,
366    R: ReplyShape<Wire: WireDocs<R, Doc>>,
367    C: Send + Sync,
368    H: Send + Sync,
369    Doc: AxisDocs<A> + Send + Sync,
370    Dest: ReplyDest<R>,
371{
372    type Input = A::Kind;
373    type Injections = ();
374    type Reply = R;
375    type Context = C;
376    // See the eager cells: the settings builder carries the real source.
377    type Source = Unnamed<Name>;
378
379    fn source(&self) -> Unnamed<Name> {
380        Unnamed::new()
381    }
382
383    fn reply_name(&self) -> &str {
384        self.0.dest.name()
385    }
386
387    fn description(&self) -> Option<&str> {
388        self.0.value.docs.description()
389    }
390
391    fn input_schema(&self) -> Option<String> {
392        self.0
393            .value
394            .docs
395            .input_schema
396            .clone()
397            .or_else(Doc::payload_schema)
398    }
399
400    fn headers_schema(&self) -> Option<String> {
401        self.0
402            .value
403            .docs
404            .headers_schema
405            .clone()
406            .or_else(Doc::headers_schema)
407    }
408
409    fn message_name(&self) -> Option<&'static str> {
410        self.0.value.docs.message_name
411    }
412
413    fn message_description(&self) -> Option<&'static str> {
414        self.0.value.docs.message_description
415    }
416
417    fn outgoing(&self) -> Vec<OutgoingMessageMetadata> {
418        if let Some(declared) = &self.0.value.docs.outgoing {
419            return declared.clone();
420        }
421        vec![
422            OutgoingMessageMetadata::new(self.reply_name().to_owned(), type_name::<R::Body>())
423                .with_payload_schema(<R::Wire as WireDocs<R, Doc>>::payload_schema())
424                .with_headers_schema(<R::Wire as WireDocs<R, Doc>>::headers_schema())
425                .with_serialized(<R::Wire as WireDocs<R, Doc>>::SERIALIZED)
426                .with_kind(OutgoingKind::Answer),
427        ]
428    }
429}
430
431impl<T, R, C, S, H, Doc, Dest> PublishingCall<S>
432    for Sealed<ReplyValue<HandleValue<Solo<T>, R, (), C, H, Doc>, Dest>>
433where
434    Self: PublishingDef<Input = <Solo<T> as Axis>::Kind, Injections = (), Reply = R, Context = C>,
435    T: Input<Axis = Solo<T>> + Send + Sync + 'static,
436    R: ReplyShape,
437    C: Send + Sync,
438    S: Send + Sync,
439    H: Handle<T, R, (), C, S>,
440{
441    async fn call(
442        &self,
443        input: &T,
444        _injections: &(),
445        ctx: &mut Context<'_, C, S>,
446    ) -> Result<R, HandlerOutcome> {
447        self.0.value.body.handle(input, &(), ctx).await
448    }
449}
450
451impl<F, R, C, S, H, Doc, Dest> PublishingCall<S>
452    for Sealed<ReplyValue<HandleValue<SoloDeserialized<F>, R, (), C, H, Doc>, Dest>>
453where
454    Self: PublishingDef<
455            Input = <SoloDeserialized<F> as Axis>::Kind,
456            Injections = (),
457            Reply = R,
458            Context = C,
459        >,
460    F: Deserialized + Send + Sync + 'static,
461    for<'p> F::Output<'p>: Input<Axis = SoloDeserialized<F>>,
462    R: ReplyShape,
463    C: Send + Sync,
464    S: Send + Sync,
465    H: for<'p> Handle<F::Output<'p>, R, (), C, S>,
466{
467    async fn call(
468        &self,
469        input: &[u8],
470        _injections: &(),
471        ctx: &mut Context<'_, C, S>,
472    ) -> Result<R, HandlerOutcome> {
473        let input = construct::<F, C, S>(input, ctx)?;
474        self.0.value.body.handle(&input, &(), ctx).await
475    }
476}
477
478impl<Hd, P, R, C, S, H, Doc, Dest> PublishingCall<S>
479    for Sealed<ReplyValue<HandleValue<SoloPair<Hd, P>, R, (), C, H, Doc>, Dest>>
480where
481    Self: PublishingDef<
482            Input = <SoloPair<Hd, P> as Axis>::Kind,
483            Injections = (),
484            Reply = R,
485            Context = C,
486        >,
487    Message<Hd, P>: Input<Axis = SoloPair<Hd, P>>,
488    Hd: Send + Sync + 'static,
489    P: Send + Sync + 'static,
490    R: ReplyShape,
491    C: Send + Sync,
492    S: Send + Sync,
493    H: Handle<Message<Hd, P>, R, (), C, S>,
494{
495    async fn call(
496        &self,
497        input: &Message<Hd, P>,
498        _injections: &(),
499        ctx: &mut Context<'_, C, S>,
500    ) -> Result<R, HandlerOutcome> {
501        self.0.value.body.handle(input, &(), ctx).await
502    }
503}
504impl<A, R, C, H, Doc, Dest> BatchPublishingDef
505    for Sealed<ReplyValue<HandleValue<A, Vec<R>, (), C, H, Doc>, Dest>>
506where
507    A: BatchedAxis,
508    R: ReplyShape<Wire: WireDocs<R, Doc>>,
509    C: Send + Sync,
510    H: Send + Sync,
511    Doc: AxisDocs<A> + Send + Sync,
512    Dest: ReplyDest<R>,
513{
514    type Input = A::Kind;
515    type Injections = ();
516    type Context = C;
517    type Reply = R;
518    type Source = Unnamed<Name>;
519
520    fn source(&self) -> Unnamed<Name> {
521        Unnamed::new()
522    }
523
524    fn reply_name(&self) -> &str {
525        self.0.dest.name()
526    }
527
528    fn description(&self) -> Option<&str> {
529        self.0.value.docs.description()
530    }
531
532    fn input_schema(&self) -> Option<String> {
533        self.0
534            .value
535            .docs
536            .input_schema
537            .clone()
538            .or_else(Doc::payload_schema)
539    }
540
541    fn headers_schema(&self) -> Option<String> {
542        self.0
543            .value
544            .docs
545            .headers_schema
546            .clone()
547            .or_else(Doc::headers_schema)
548    }
549
550    fn message_name(&self) -> Option<&'static str> {
551        self.0.value.docs.message_name
552    }
553
554    fn message_description(&self) -> Option<&'static str> {
555        self.0.value.docs.message_description
556    }
557
558    fn outgoing(&self) -> Vec<OutgoingMessageMetadata> {
559        if let Some(declared) = &self.0.value.docs.outgoing {
560            return declared.clone();
561        }
562        vec![
563            OutgoingMessageMetadata::new(self.reply_name().to_owned(), type_name::<R::Body>())
564                .with_payload_schema(<R::Wire as WireDocs<R, Doc>>::payload_schema())
565                .with_headers_schema(<R::Wire as WireDocs<R, Doc>>::headers_schema())
566                .with_serialized(<R::Wire as WireDocs<R, Doc>>::SERIALIZED)
567                .with_kind(OutgoingKind::Answer),
568        ]
569    }
570}
571
572/// Applies the batch reply contract: one reply per element, or one outcome per element.
573pub(super) fn batch_reply_verdict<R>(
574    verdict: Result<Vec<R>, Vec<HandlerOutcome>>,
575    batch_len: usize,
576    subscription: &str,
577) -> Result<Vec<R>, BatchResult> {
578    match verdict {
579        Ok(replies) => {
580            assert!(
581                replies.len() == batch_len,
582                "subscriber '{subscription}' returned {} replies for a batch of {batch_len}",
583                replies.len(),
584            );
585            Ok(replies)
586        }
587        Err(outcomes) => {
588            assert!(
589                outcomes.len() == batch_len,
590                "subscriber '{subscription}' returned {} per-element outcomes for a batch of \
591                 {batch_len}",
592                outcomes.len(),
593            );
594            Err(BatchResult::PerElement(outcomes))
595        }
596    }
597}
598
599impl<T, R, C, S, H, Doc, Dest> BatchPublishingCall<S>
600    for Sealed<ReplyValue<HandleValue<Batch<T>, Vec<R>, (), C, H, Doc>, Dest>>
601where
602    Self: BatchPublishingDef<
603            Input = <Batch<T> as Axis>::Kind,
604            Injections = (),
605            Context = C,
606            Reply = R,
607        >,
608    [T]: Input<Axis = Batch<T>>,
609    T: Send + Sync + 'static,
610    R: ReplyShape,
611    C: Send + Sync,
612    S: Send + Sync,
613    H: Handle<[T], Vec<R>, (), C, S>,
614{
615    async fn call(
616        &self,
617        batch: &[T],
618        _injections: &(),
619        ctx: &mut Context<'_, C, S>,
620    ) -> Result<Vec<R>, BatchResult> {
621        let verdict = self.0.value.body.handle(batch, &(), ctx).await;
622        batch_reply_verdict(verdict, batch.len(), ctx.name())
623    }
624}
625
626impl<Hd, P, R, C, S, H, Doc, Dest> BatchPublishingCall<S>
627    for Sealed<ReplyValue<HandleValue<BatchPair<Hd, P>, Vec<R>, (), C, H, Doc>, Dest>>
628where
629    Self: BatchPublishingDef<
630            Input = <BatchPair<Hd, P> as Axis>::Kind,
631            Injections = (),
632            Context = C,
633            Reply = R,
634        >,
635    [Message<Hd, P>]: Input<Axis = BatchPair<Hd, P>>,
636    Hd: Send + Sync + 'static,
637    P: Send + Sync + 'static,
638    R: ReplyShape,
639    C: Send + Sync,
640    S: Send + Sync,
641    H: Handle<[Message<Hd, P>], Vec<R>, (), C, S>,
642{
643    async fn call(
644        &self,
645        batch: &[Message<Hd, P>],
646        _injections: &(),
647        ctx: &mut Context<'_, C, S>,
648    ) -> Result<Vec<R>, BatchResult> {
649        let verdict = self.0.value.body.handle(batch, &(), ctx).await;
650        batch_reply_verdict(verdict, batch.len(), ctx.name())
651    }
652}