Skip to main content

interlink/
route.rs

1//! Bus routing addresses.
2//!
3//! A [`Route`] is an identity key optionally scoped to one live session
4//! (`key#session_id`). The bus treats the whole thing as an **opaque** queue name —
5//! it never parses the `#`. This module is the *one* place that builds and splits
6//! the convention, so the client (and the bus's roster key) agree by construction
7//! instead of by scattered `format!` / `split` calls.
8
9use std::fmt;
10
11/// An inbox address: a base64 identity `key`, optionally scoped to a `session`.
12/// Keys are base64 and session ids are hex, so neither contains `#` — the single
13/// separator is unambiguous.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Route {
16    pub key: String,
17    pub session: Option<String>,
18}
19
20impl Route {
21    /// A session-scoped route. An empty `session` collapses to a bare-key route, so
22    /// a legacy/sessionless announcement round-trips to `key`.
23    pub fn new(key: impl Into<String>, session: impl Into<String>) -> Self {
24        let session = session.into();
25        Route {
26            key: key.into(),
27            session: (!session.is_empty()).then_some(session),
28        }
29    }
30
31    /// A bare-key route (no session scope).
32    pub fn bare(key: impl Into<String>) -> Self {
33        Route {
34            key: key.into(),
35            session: None,
36        }
37    }
38
39    /// Parse `key#session` (or a bare `key`). A trailing empty session (`key#`) is
40    /// treated as bare.
41    pub fn parse(s: &str) -> Self {
42        match s.split_once('#') {
43            Some((key, session)) if !session.is_empty() => Route {
44                key: key.to_string(),
45                session: Some(session.to_string()),
46            },
47            _ => Route::bare(s),
48        }
49    }
50
51    /// The session scope, if any.
52    pub fn session(&self) -> Option<&str> {
53        self.session.as_deref()
54    }
55}
56
57impl fmt::Display for Route {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match &self.session {
60            Some(s) => write!(f, "{}#{}", self.key, s),
61            None => write!(f, "{}", self.key),
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn round_trips_session_scoped() {
72        let r = Route::new("KEY", "s12");
73        assert_eq!(r.to_string(), "KEY#s12");
74        assert_eq!(Route::parse("KEY#s12"), r);
75        assert_eq!(r.session(), Some("s12"));
76    }
77
78    #[test]
79    fn empty_session_is_bare() {
80        assert_eq!(Route::new("KEY", ""), Route::bare("KEY"));
81        assert_eq!(Route::new("KEY", "").to_string(), "KEY");
82        assert_eq!(Route::parse("KEY").session(), None);
83        assert_eq!(Route::parse("KEY#").session(), None, "trailing # is bare");
84    }
85
86    #[test]
87    fn parse_splits_on_the_separator() {
88        let r = Route::parse("KEY#abcd");
89        assert_eq!(r.key, "KEY");
90        assert_eq!(r.session(), Some("abcd"));
91    }
92}