1use alloc::{string::String, vec::Vec};
4
5use crate::{Command, Handled, Instant, Link, Message};
6
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
10#[non_exhaustive]
11pub enum Receipt {
12 Subscribed {
14 filters: Vec<String>,
16 granted: bool,
18 },
19 Written {
21 topic: String,
23 },
24 Acknowledged {
26 topic: String,
28 cleared: bool,
30 },
31}
32
33pub trait Role {
39 fn handle_link(&mut self, now: Instant, link: Link);
41 fn handle_message(&mut self, now: Instant, msg: Message<'_>) -> Handled;
43 fn handle_command_failed(&mut self, now: Instant, cmd: Command);
45 fn handle_timeout(&mut self, now: Instant);
47 fn handle_dropped(&mut self, _now: Instant, _topic: &str) {}
49 fn handle_lost(&mut self, _topic: &str) {}
52 fn handle_receipt(&mut self, _now: Instant, _receipt: &Receipt) {}
54 fn poll_command(&mut self) -> Option<Command>;
56 fn poll_timeout(&self) -> Option<Instant>;
58 fn wants(&self, topic: &str) -> bool;
60 fn owns(&self, cmd: &Command) -> bool;
62 fn holds(&self, filter: &str) -> bool;
64}
65
66#[derive(Debug)]
79pub struct Pair<A: Role, B: Role> {
80 pub a: A,
82 pub b: B,
84 up: bool,
85}
86
87impl<A: Role, B: Role> Pair<A, B> {
88 pub fn new(a: A, b: B) -> Self {
90 Pair { a, b, up: false }
91 }
92
93 pub fn is_up(&self) -> bool {
95 self.up
96 }
97}
98
99fn strip(cmd: Command, other: &impl Role) -> Option<Command> {
101 match cmd {
102 Command::Unsubscribe { filters } => {
103 let filters: Vec<String> = filters.into_iter().filter(|f| !other.holds(f)).collect();
104 (!filters.is_empty()).then_some(Command::Unsubscribe { filters })
105 }
106 cmd => Some(cmd),
107 }
108}
109
110impl<A: Role, B: Role> Role for Pair<A, B> {
111 fn handle_link(&mut self, now: Instant, link: Link) {
112 self.up = matches!(link, Link::Up { .. });
113 self.a.handle_link(now, link);
114 self.b.handle_link(now, link);
115 }
116
117 fn handle_message(&mut self, now: Instant, msg: Message<'_>) -> Handled {
118 let (wa, wb) = (self.a.wants(msg.topic), self.b.wants(msg.topic));
119 if !wa && !wb {
120 return Handled::Ignored;
121 }
122 if !self.up {
123 self.handle_link(
124 now,
125 Link::Up {
126 session_present: false,
127 },
128 );
129 }
130 let mut handled = Handled::Ignored;
131 if wa && self.a.handle_message(now, msg) == Handled::Consumed {
132 handled = Handled::Consumed;
133 }
134 if wb && self.b.handle_message(now, msg) == Handled::Consumed {
135 handled = Handled::Consumed;
136 }
137 handled
138 }
139
140 fn handle_command_failed(&mut self, now: Instant, cmd: Command) {
141 if self.a.owns(&cmd) {
142 self.a.handle_command_failed(now, cmd);
143 } else if self.b.owns(&cmd) {
144 self.b.handle_command_failed(now, cmd);
145 }
146 }
147
148 fn handle_timeout(&mut self, now: Instant) {
149 self.a.handle_timeout(now);
150 self.b.handle_timeout(now);
151 }
152
153 fn handle_dropped(&mut self, now: Instant, topic: &str) {
154 self.a.handle_dropped(now, topic);
155 self.b.handle_dropped(now, topic);
156 }
157
158 fn handle_lost(&mut self, topic: &str) {
159 self.a.handle_lost(topic);
160 self.b.handle_lost(topic);
161 }
162
163 fn handle_receipt(&mut self, now: Instant, receipt: &Receipt) {
164 self.a.handle_receipt(now, receipt);
165 self.b.handle_receipt(now, receipt);
166 }
167
168 fn poll_command(&mut self) -> Option<Command> {
169 while let Some(cmd) = self.a.poll_command() {
170 if let Some(cmd) = strip(cmd, &self.b) {
171 return Some(cmd);
172 }
173 }
174 while let Some(cmd) = self.b.poll_command() {
175 if let Some(cmd) = strip(cmd, &self.a) {
176 return Some(cmd);
177 }
178 }
179 None
180 }
181
182 fn poll_timeout(&self) -> Option<Instant> {
183 match (self.a.poll_timeout(), self.b.poll_timeout()) {
184 (Some(x), Some(y)) => Some(x.min(y)),
185 (x, y) => x.or(y),
186 }
187 }
188
189 fn wants(&self, topic: &str) -> bool {
190 self.a.wants(topic) || self.b.wants(topic)
191 }
192
193 fn owns(&self, cmd: &Command) -> bool {
194 self.a.owns(cmd) || self.b.owns(cmd)
195 }
196
197 fn holds(&self, filter: &str) -> bool {
198 self.a.holds(filter) || self.b.holds(filter)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 extern crate std;
205
206 use super::*;
207 use crate::{FilterLedger, QoS};
208 use alloc::{collections::VecDeque, string::ToString, vec, vec::Vec};
209
210 #[derive(Default)]
212 struct Fake {
213 name: &'static str,
214 prefix: &'static str,
215 ledger: FilterLedger,
216 queue: VecDeque<Command>,
217 log: Vec<std::string::String>,
218 timeout: Option<Instant>,
219 }
220
221 impl Fake {
222 fn new(name: &'static str, prefix: &'static str) -> Self {
223 Fake {
224 name,
225 prefix,
226 ..Fake::default()
227 }
228 }
229
230 fn sub(&mut self, filters: &[&str]) {
231 let cmd = self.ledger.subscribe(
232 filters
233 .iter()
234 .map(|f| (f.to_string(), QoS::ExactlyOnce))
235 .collect(),
236 );
237 self.queue.extend(cmd);
238 }
239
240 fn unsub(&mut self, filters: &[&str]) {
241 let cmd = self
242 .ledger
243 .unsubscribe(filters.iter().map(|f| f.to_string()).collect());
244 self.queue.extend(cmd);
245 }
246
247 fn publish(&mut self, topic: &str) {
248 self.queue.push_back(Command::Publish {
249 topic: topic.to_string(),
250 payload: Vec::new(),
251 qos: QoS::AtMostOnce,
252 retain: false,
253 });
254 }
255 }
256
257 impl Role for Fake {
258 fn handle_link(&mut self, _now: Instant, link: Link) {
259 self.log.push(std::format!("{}:{link:?}", self.name));
260 }
261 fn handle_message(&mut self, _now: Instant, msg: Message<'_>) -> Handled {
262 self.log
263 .push(std::format!("{}:msg {}", self.name, msg.topic));
264 Handled::Consumed
265 }
266 fn handle_command_failed(&mut self, _now: Instant, cmd: Command) {
267 self.log.push(std::format!("{}:failed {cmd:?}", self.name));
268 }
269 fn handle_timeout(&mut self, _now: Instant) {
270 self.log.push(std::format!("{}:timeout", self.name));
271 }
272 fn handle_dropped(&mut self, _now: Instant, topic: &str) {
273 self.log.push(std::format!("{}:dropped {topic}", self.name));
274 }
275 fn handle_receipt(&mut self, _now: Instant, receipt: &Receipt) {
276 self.log
277 .push(std::format!("{}:receipt {receipt:?}", self.name));
278 }
279 fn poll_command(&mut self) -> Option<Command> {
280 self.queue.pop_front()
281 }
282 fn poll_timeout(&self) -> Option<Instant> {
283 self.timeout
284 }
285 fn wants(&self, topic: &str) -> bool {
286 topic.starts_with(self.prefix)
287 }
288 fn owns(&self, cmd: &Command) -> bool {
289 match cmd {
290 Command::Publish { topic, .. } => topic.starts_with(self.prefix),
291 _ => false,
292 }
293 }
294 fn holds(&self, filter: &str) -> bool {
295 self.ledger.count(filter) > 0
296 }
297 }
298
299 fn pair() -> Pair<Fake, Fake> {
300 Pair::new(Fake::new("dev", "d/"), Fake::new("ctl", "c/"))
301 }
302
303 fn drain(p: &mut Pair<Fake, Fake>) -> Vec<Command> {
304 core::iter::from_fn(|| p.poll_command()).collect()
305 }
306
307 fn unsub_cmd(filters: &[&str]) -> Command {
308 Command::Unsubscribe {
309 filters: filters.iter().map(|f| f.to_string()).collect(),
310 }
311 }
312
313 #[test]
314 fn link_and_commands_go_to_a_first() {
315 let mut p = pair();
316 p.b.publish("c/1");
317 p.a.publish("d/1");
318 p.a.publish("d/2");
319 p.handle_link(
320 Instant(0),
321 Link::Up {
322 session_present: true,
323 },
324 );
325 assert_eq!(
326 p.a.log,
327 ["dev:Up { session_present: true }"],
328 "a sees the link"
329 );
330 assert_eq!(p.b.log, ["ctl:Up { session_present: true }"]);
331 let topics: Vec<_> = drain(&mut p)
332 .into_iter()
333 .map(|c| match c {
334 Command::Publish { topic, .. } => topic,
335 other => std::format!("{other:?}"),
336 })
337 .collect();
338 assert_eq!(topics, ["d/1", "d/2", "c/1"]);
339 }
340
341 #[test]
342 fn unsubscribe_keeps_filters_the_other_role_holds() {
343 let mut p = pair();
344 p.a.sub(&["x/$state", "d/+/+/set"]);
345 p.b.sub(&["x/$state", "x/$description"]);
346 drain(&mut p);
347 p.b.unsub(&["x/$state", "x/$description"]);
349 assert_eq!(drain(&mut p), [unsub_cmd(&["x/$description"])]);
350 p.a.unsub(&["x/$state"]);
352 assert_eq!(drain(&mut p), [unsub_cmd(&["x/$state"])]);
353 }
354
355 #[test]
356 fn unsubscribe_entirely_held_by_the_other_role_is_dropped() {
357 let mut p = pair();
358 p.a.sub(&["x/$state"]);
359 p.b.sub(&["x/$state"]);
360 drain(&mut p);
361 p.a.unsub(&["x/$state"]);
362 p.a.publish("d/after");
363 let cmds = drain(&mut p);
364 assert_eq!(cmds.len(), 1);
365 assert!(matches!(&cmds[0], Command::Publish { topic, .. } if topic == "d/after"));
366 assert!(p.holds("x/$state"));
367 }
368
369 #[test]
370 fn messages_go_to_every_role_that_wants_them() {
371 let mut p = pair();
372 p.handle_link(
373 Instant(0),
374 Link::Up {
375 session_present: false,
376 },
377 );
378 let msg = |topic| Message {
379 topic,
380 payload: b"",
381 retain: None,
382 };
383 assert_eq!(p.handle_message(Instant(0), msg("d/x")), Handled::Consumed);
384 assert_eq!(p.handle_message(Instant(0), msg("c/x")), Handled::Consumed);
385 assert_eq!(p.handle_message(Instant(0), msg("z/x")), Handled::Ignored);
386 assert_eq!(p.a.log.last().map(|s| s.as_str()), Some("dev:msg d/x"));
387 assert_eq!(p.b.log.last().map(|s| s.as_str()), Some("ctl:msg c/x"));
388 assert_eq!(p.a.log.len(), 2);
389 assert_eq!(p.b.log.len(), 2);
390 }
391
392 #[test]
393 fn a_message_while_down_brings_both_roles_up_first() {
394 let mut p = pair();
395 let msg = Message {
396 topic: "c/x",
397 payload: b"1",
398 retain: Some(false),
399 };
400 p.handle_message(Instant(5), msg);
401 assert!(p.is_up());
402 assert_eq!(p.a.log, ["dev:Up { session_present: false }"]);
403 assert_eq!(
404 p.b.log,
405 ["ctl:Up { session_present: false }", "ctl:msg c/x"]
406 );
407 p.handle_message(Instant(6), msg);
409 assert_eq!(p.a.log.len(), 1);
410 p.handle_link(Instant(7), Link::Down);
412 p.handle_message(
413 Instant(8),
414 Message {
415 topic: "z/x",
416 ..msg
417 },
418 );
419 assert!(!p.is_up());
420 }
421
422 #[test]
423 fn failed_command_goes_to_its_owner() {
424 let mut p = pair();
425 let publish = |topic: &str| Command::Publish {
426 topic: topic.to_string(),
427 payload: vec![],
428 qos: QoS::AtMostOnce,
429 retain: false,
430 };
431 p.handle_command_failed(Instant(0), publish("c/1"));
432 p.handle_command_failed(Instant(0), publish("d/1"));
433 p.handle_command_failed(Instant(0), publish("z/1"));
434 assert_eq!(p.a.log.len(), 1);
435 assert!(p.a.log[0].contains("d/1"));
436 assert_eq!(p.b.log.len(), 1);
437 assert!(p.b.log[0].contains("c/1"));
438 }
439
440 #[test]
441 fn receipts_drops_and_timeouts_reach_both_roles() {
442 let mut p = pair();
443 let r = Receipt::Written {
444 topic: "d/1".to_string(),
445 };
446 p.handle_receipt(Instant(0), &r);
447 p.handle_dropped(Instant(0), "c/x");
448 p.handle_timeout(Instant(0));
449 for log in [&p.a.log, &p.b.log] {
450 assert_eq!(log.len(), 3);
451 assert!(log[0].contains("Written"));
452 assert!(log[1].contains("dropped c/x"));
453 assert!(log[2].ends_with("timeout"));
454 }
455 }
456
457 #[test]
458 fn poll_timeout_is_the_earlier_one() {
459 let mut p = pair();
460 assert_eq!(p.poll_timeout(), None);
461 p.b.timeout = Some(Instant(9));
462 assert_eq!(p.poll_timeout(), Some(Instant(9)));
463 p.a.timeout = Some(Instant(4));
464 assert_eq!(p.poll_timeout(), Some(Instant(4)));
465 }
466}