Skip to main content

ruststream_lapin/
reply.rs

1//! The responder half of the direct reply-to convention, packaged as a publish transform.
2
3use ruststream::Str;
4use ruststream::runtime::{ForReply, Names, Outgoing, PublishContext, PublishTransform};
5
6/// Redirects each reply of a `#[subscriber(.., reply(..))]` handler to the requester's
7/// private reply-to address, echoing its correlation id.
8///
9/// This is the canonical responder wiring for [request/reply over `RabbitMQ` direct
10/// reply-to](crate::LapinRequester): compose it onto the reply publisher at mount time and the
11/// handler stays a pure request-to-reply function. A request without a `reply-to` header falls
12/// through to the mount site's `reply("..")` name.
13///
14/// The transform names the destination per delivery, so it mounts only where nothing has declared
15/// one: the reply type derives `Outgoing` without `#[outgoing(name = "..")]` and the mount site
16/// supplies the fallback name. A reply type that declares its own destination refuses this
17/// transform at the mount site, which is what keeps the generated `AsyncAPI` document and the wire
18/// in step.
19///
20/// It writes none of the per-message AMQP properties, so it is generic over the publisher's
21/// options and mounts over any of this crate's publishers. The reply-to address and the
22/// correlation id it echoes are protocol fields the crate carries as headers, and the publisher
23/// puts them back on the frame.
24///
25/// # Examples
26///
27/// ```
28/// use ruststream_lapin::prelude::*;
29/// use serde::{Deserialize, Serialize};
30///
31/// #[derive(Deserialize)]
32/// struct Ask {
33///     sku: String,
34/// }
35///
36/// // An RPC reply goes wherever the request asked, so the destination belongs to the mount
37/// // site and the type declares none.
38/// #[derive(Serialize, Outgoing)]
39/// struct Stock {
40///     available: bool,
41/// }
42///
43/// #[subscriber("inventory.check", reply("inventory.check.unrouted"))]
44/// async fn check(ask: &Ask) -> Stock {
45///     Stock {
46///         available: !ask.sku.is_empty(),
47///     }
48/// }
49///
50/// fn app() -> RustStream {
51///     let broker = LapinBroker::new("amqp://localhost:5672");
52///     RustStream::new(AppInfo::new("inventory", "0.1.0")).with_broker(broker, |b| {
53///         b.include(check)
54///             .out_reply(Publish::default())
55///             .transform(DirectReplyTo);
56///     })
57/// }
58/// ```
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
60pub struct DirectReplyTo;
61
62// --8<-- [start:transform]
63impl<C, Options> PublishTransform<ForReply<C>, Options> for DirectReplyTo {
64    type Destination = Names;
65
66    fn apply(
67        &self,
68        out: &mut Outgoing<'_>,
69        _options: &mut Option<Options>,
70        cx: &PublishContext<'_, C>,
71    ) {
72        // Both fields travel as the buffers the delivery already holds: the reply takes a
73        // reference count on the request's bytes instead of copying them per message. A reply-to
74        // that is not UTF-8 is not an address, and falls through to the mount site's name.
75        if let Some(reply_to) = cx.headers().get_shared("reply-to")
76            && let Ok(name) = Str::try_from(reply_to)
77        {
78            out.set_name(name);
79        }
80        if let Some(correlation_id) = cx.headers().get_shared("correlation-id") {
81            out.headers_mut()
82                .insert(Str::from_static("correlation-id"), correlation_id);
83        }
84    }
85}
86// --8<-- [end:transform]