Skip to main content

pamoja_loopback/
transport.rs

1//! The loopback transport itself.
2
3use std::sync::{Arc, Mutex};
4
5use tokio::sync::mpsc::{self, UnboundedReceiver};
6
7use pamoja_core::{Error, Result, Transport};
8
9use crate::broker::LoopbackBroker;
10
11/// A message delivered over a loopback subscription.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct Message {
14    /// The topic the message was published to.
15    pub topic: String,
16    /// The raw payload bytes.
17    pub payload: Vec<u8>,
18}
19
20/// An in-process transport that routes through a shared [`LoopbackBroker`].
21///
22/// A transport is created disconnected; [`connect`](Transport::connect) registers
23/// it with the broker so it can publish and receive. Inbound messages are read
24/// with [`recv`](LoopbackTransport::recv).
25///
26/// # Examples
27///
28/// ```
29/// use pamoja_core::Transport;
30/// use pamoja_loopback::{LoopbackBroker, LoopbackTransport};
31///
32/// # async fn run() -> pamoja_core::Result<()> {
33/// let broker = LoopbackBroker::new();
34/// let mut subscriber = LoopbackTransport::new(broker.clone());
35/// let mut publisher = LoopbackTransport::new(broker);
36/// subscriber.connect().await?;
37/// publisher.connect().await?;
38///
39/// subscriber.subscribe("sensors/+/temperature").await?;
40/// publisher.send("sensors/1/temperature", b"21.5").await?;
41///
42/// let message = subscriber.recv().await?.expect("a message");
43/// assert_eq!(message.topic, "sensors/1/temperature");
44/// assert_eq!(message.payload, b"21.5");
45/// # Ok(())
46/// # }
47/// ```
48pub struct LoopbackTransport {
49    broker: LoopbackBroker,
50    filters: Arc<Mutex<Vec<String>>>,
51    incoming: Option<UnboundedReceiver<Message>>,
52}
53
54impl LoopbackTransport {
55    /// Creates a disconnected transport bound to `broker`.
56    ///
57    /// # Arguments
58    ///
59    /// * `broker` - the shared broker this transport publishes to and receives from.
60    ///
61    /// # Returns
62    ///
63    /// A disconnected transport ready for [`connect`](Transport::connect).
64    pub fn new(broker: LoopbackBroker) -> Self {
65        Self {
66            broker,
67            filters: Arc::new(Mutex::new(Vec::new())),
68            incoming: None,
69        }
70    }
71
72    /// Reports whether the transport is connected to its broker.
73    ///
74    /// # Returns
75    ///
76    /// `true` once [`connect`](Transport::connect) has succeeded and before
77    /// [`disconnect`](LoopbackTransport::disconnect) is called.
78    pub fn is_connected(&self) -> bool {
79        self.incoming.is_some()
80    }
81
82    /// Awaits the next message from any subscribed topic.
83    ///
84    /// # Returns
85    ///
86    /// `Some(message)` for the next message, or `None` once the broker and all
87    /// other transports have been dropped.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`Error::Closed`](pamoja_core::Error::Closed) if the transport is
92    /// not connected.
93    pub async fn recv(&mut self) -> Result<Option<Message>> {
94        let incoming = self.incoming.as_mut().ok_or(Error::Closed)?;
95        Ok(incoming.recv().await)
96    }
97
98    /// Disconnects the transport from the broker.
99    ///
100    /// Its registration is pruned from the broker on the next publish.
101    pub fn disconnect(&mut self) {
102        self.incoming = None;
103    }
104}
105
106impl Transport for LoopbackTransport {
107    async fn connect(&mut self) -> Result<()> {
108        let (sender, receiver) = mpsc::unbounded_channel();
109        self.broker.register(Arc::clone(&self.filters), sender);
110        self.incoming = Some(receiver);
111        Ok(())
112    }
113
114    async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
115        if self.incoming.is_none() {
116            return Err(Error::Closed);
117        }
118        self.broker.publish(&Message {
119            topic: topic.to_owned(),
120            payload: payload.to_vec(),
121        });
122        Ok(())
123    }
124
125    async fn subscribe(&mut self, topic: &str) -> Result<()> {
126        if self.incoming.is_none() {
127            return Err(Error::Closed);
128        }
129        self.filters
130            .lock()
131            .expect("filters lock")
132            .push(topic.to_owned());
133        Ok(())
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[tokio::test]
142    async fn publish_and_subscribe_round_trip() {
143        let broker = LoopbackBroker::new();
144        let mut subscriber = LoopbackTransport::new(broker.clone());
145        let mut publisher = LoopbackTransport::new(broker);
146        subscriber.connect().await.expect("connect");
147        publisher.connect().await.expect("connect");
148
149        subscriber
150            .subscribe("sensors/+/temperature")
151            .await
152            .expect("subscribe");
153        publisher
154            .send("sensors/1/temperature", b"21.5")
155            .await
156            .expect("send");
157
158        let message = subscriber.recv().await.expect("recv").expect("a message");
159        assert_eq!(message.topic, "sensors/1/temperature");
160        assert_eq!(message.payload, b"21.5");
161    }
162
163    #[tokio::test]
164    async fn non_matching_topics_are_not_delivered() {
165        let broker = LoopbackBroker::new();
166        let mut subscriber = LoopbackTransport::new(broker.clone());
167        let mut publisher = LoopbackTransport::new(broker);
168        subscriber.connect().await.expect("connect");
169        publisher.connect().await.expect("connect");
170
171        subscriber
172            .subscribe("sensors/1/#")
173            .await
174            .expect("subscribe");
175        publisher
176            .send("sensors/2/temperature", b"x")
177            .await
178            .expect("send");
179        publisher
180            .send("sensors/1/humidity", b"y")
181            .await
182            .expect("send");
183
184        let message = subscriber.recv().await.expect("recv").expect("a message");
185        assert_eq!(message.topic, "sensors/1/humidity");
186    }
187
188    #[tokio::test]
189    async fn operations_before_connect_report_closed() {
190        let broker = LoopbackBroker::new();
191        let mut transport = LoopbackTransport::new(broker);
192        assert!(matches!(
193            transport.send("t", b"x").await,
194            Err(Error::Closed)
195        ));
196        assert!(matches!(transport.subscribe("t").await, Err(Error::Closed)));
197        assert!(matches!(transport.recv().await, Err(Error::Closed)));
198        assert!(!transport.is_connected());
199    }
200}