Skip to main content

ruststream_rumqttc/
context.rs

1//! [`MqttContext`]: what a delivery carries beyond its payload and headers.
2//!
3//! MQTT publishes to a topic and subscribes with a filter, so the two are not the same string
4//! whenever a filter carries a wildcard. The subscription name a handler reads with `ctx.name()`
5//! is the filter; the topic the message was actually published to is here, under the
6//! [`DeliveryTopic`] key.
7
8use ruststream::{BuildContext, ContextField, Field};
9
10use crate::message::MqttMessage;
11
12/// The per-delivery context of this broker.
13///
14/// Read a field with a key rather than by field access: `ctx.context(DeliveryTopic)` in a handler
15/// body, `cx.context(DeliveryTopic)` in a publish transform on the reply or the retry position.
16///
17/// # Examples
18///
19/// ```
20/// # mod demo {
21/// use ruststream_rumqttc::prelude::*;
22/// use serde::Deserialize;
23///
24/// #[derive(Deserialize)]
25/// struct Telemetry {
26///     temperature: f64,
27/// }
28///
29/// #[subscriber(MqttFilter::new("devices/+/telemetry"))]
30/// async fn record(telemetry: &Telemetry, ctx: &mut Context<'_, MqttContext>) -> HandlerOutcome {
31///     let device = ctx.context(DeliveryTopic).split('/').nth(1).unwrap_or_default();
32///     tracing::info!(device, telemetry.temperature, "reading recorded");
33///     HandlerOutcome::ack()
34/// }
35///
36/// #[ruststream::app]
37/// fn app() -> impl App {
38///     RustStream::new(AppInfo::new("telemetry", "0.1.0")).with_broker(
39///         MqttBroker::new("mqtt://localhost:1883", "telemetry-svc"),
40///         |b| {
41///             b.include(record)
42///                 .out_retry(Publish::default())
43///                 .to("devices/retry/telemetry");
44///         },
45///     )
46/// }
47/// # }
48/// # fn main() {}
49/// ```
50#[derive(Debug, Clone, PartialEq, Eq)]
51#[non_exhaustive]
52pub struct MqttContext {
53    topic: String,
54}
55
56impl MqttContext {
57    /// Builds the context of a delivery that arrived on `topic`.
58    #[must_use]
59    pub fn new(topic: impl Into<String>) -> Self {
60        Self {
61            topic: topic.into(),
62        }
63    }
64
65    /// The concrete topic this delivery was published to, never the filter that matched it.
66    #[must_use]
67    pub fn topic(&self) -> &str {
68        &self.topic
69    }
70}
71
72/// The key for [`MqttContext::topic`].
73///
74/// It reads the topic borrowed through `ctx.context(DeliveryTopic)`, and owned through the
75/// `Ctx<DeliveryTopic>` extractor, which is also what fixes a handler's context type to
76/// [`MqttContext`] without a `Context` parameter.
77///
78/// # Examples
79///
80/// ```
81/// # #[cfg(feature = "testing")]
82/// # mod demo {
83/// use std::error::Error;
84///
85/// use ruststream::testing::TestApp;
86/// use ruststream_rumqttc::prelude::*;
87/// use serde::{Deserialize, Serialize};
88///
89/// #[derive(Deserialize, Serialize, Outgoing)]
90/// struct Telemetry {
91///     temperature: f64,
92/// }
93///
94/// #[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
95/// #[outgoing(name = "alerts")]
96/// struct Alert {
97///     topic: String,
98/// }
99///
100/// #[subscriber(MqttFilter::new("devices/+/telemetry"))]
101/// async fn watch(
102///     telemetry: &Telemetry,
103///     Ctx(topic): Ctx<DeliveryTopic>,
104///     Out(alerts): Out<impl Publisher>,
105/// ) -> HandlerOutcome {
106///     if telemetry.temperature <= 30.0 {
107///         return HandlerOutcome::ack();
108///     }
109///     if alerts.message(&Alert { topic }).publish().await.is_err() {
110///         return HandlerOutcome::retry();
111///     }
112///     HandlerOutcome::ack()
113/// }
114///
115/// pub fn app() -> impl App {
116///     RustStream::new(AppInfo::new("telemetry", "0.1.0")).with_broker(
117///         MqttBroker::new("mqtt://localhost:1883", "telemetry-svc"),
118///         |b| {
119///             b.include(watch)
120///                 .out(DefaultSlot, Publish::default())
121///                 .out_retry(Publish::default())
122///                 .to("devices/retry/telemetry")
123///                 .build();
124///         },
125///     )
126/// }
127///
128/// pub async fn an_alert_names_the_topic_not_the_filter() -> Result<(), Box<dyn Error>> {
129///     let tb = TestApp::start(app()).await?;
130///
131///     tb.broker::<MqttBroker>()
132///         .message(&Telemetry { temperature: 31.5 })
133///         .to("devices/dev42/telemetry")
134///         .publish()
135///         .await?;
136///
137///     tb.broker::<MqttBroker>()
138///         .published::<Alert>("alerts")
139///         .assert_called_once()
140///         .with(&Alert { topic: "devices/dev42/telemetry".to_owned() });
141///     tb.shutdown().await?;
142///     Ok(())
143/// }
144/// # }
145/// # #[cfg(feature = "testing")]
146/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
147/// #     tokio::runtime::Builder::new_multi_thread()
148/// #         .enable_all()
149/// #         .build()?
150/// #         .block_on(demo::an_alert_names_the_topic_not_the_filter())
151/// # }
152/// # #[cfg(not(feature = "testing"))]
153/// # fn main() {}
154/// ```
155#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
156pub struct DeliveryTopic;
157
158impl Field<MqttContext> for DeliveryTopic {
159    type Value<'a> = &'a str;
160
161    fn get(self, src: &MqttContext) -> &str {
162        src.topic()
163    }
164}
165
166impl ContextField for DeliveryTopic {
167    type Context = MqttContext;
168    type Value = String;
169
170    fn read(self, src: &MqttContext) -> String {
171        src.topic.clone()
172    }
173}
174
175impl BuildContext<MqttMessage> for MqttContext {
176    fn build(msg: &MqttMessage) -> Self {
177        Self::new(msg.topic())
178    }
179}