Skip to main content

hermes_core/
subject.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5// ---------------------------------------------------------------------------
6// Segment – a single typed piece of a subject (concrete or pattern)
7// ---------------------------------------------------------------------------
8
9/// Characters that are reserved and must not appear in string segments.
10/// - `.` is the display separator
11/// - `*` and `>` are wildcard tokens
12const RESERVED_CHARS: &[char] = &['.', '*', '>'];
13
14/// A single segment of a [`Subject`].
15///
16/// Concrete segments are [`Str`](Segment::Str) and [`Int`](Segment::Int).
17/// Pattern segments are [`Any`](Segment::Any) (`*`, matches one) and
18/// [`Rest`](Segment::Rest) (`>`, matches zero-or-more trailing).
19#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum Segment {
21    /// A literal string value.
22    Str(String),
23    /// A literal integer value.
24    Int(i64),
25    /// `*` – matches any single segment.
26    Any,
27    /// `>` – matches zero or more trailing segments. Must be last.
28    Rest,
29}
30
31impl Segment {
32    /// Create a string segment.
33    pub fn s(s: impl Into<String>) -> Self {
34        Self::Str(s.into())
35    }
36
37    /// Create an integer segment.
38    pub fn i(v: i64) -> Self {
39        Self::Int(v)
40    }
41
42    /// Create a single-segment wildcard (`*`).
43    pub fn any() -> Self {
44        Self::Any
45    }
46
47    /// Create a trailing wildcard (`>`).
48    pub fn rest() -> Self {
49        Self::Rest
50    }
51
52    /// `true` if this segment is a wildcard (`Any` or `Rest`).
53    pub fn is_wildcard(&self) -> bool {
54        matches!(self, Self::Any | Self::Rest)
55    }
56}
57
58impl From<&str> for Segment {
59    fn from(s: &str) -> Self {
60        Self::Str(s.to_owned())
61    }
62}
63
64impl From<String> for Segment {
65    fn from(s: String) -> Self {
66        Self::Str(s)
67    }
68}
69
70impl From<i64> for Segment {
71    fn from(v: i64) -> Self {
72        Self::Int(v)
73    }
74}
75
76impl fmt::Display for Segment {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::Str(s) => f.write_str(s),
80            Self::Int(n) => write!(f, "{n}"),
81            Self::Any => f.write_str("*"),
82            Self::Rest => f.write_str(">"),
83        }
84    }
85}
86
87// ---------------------------------------------------------------------------
88// Subject – a path of segments, both concrete and pattern
89// ---------------------------------------------------------------------------
90
91/// A structured subject made of typed [`Segment`]s.
92///
93/// A subject can be **concrete** (no wildcards) or a **pattern** (with `*`
94/// and/or `>`). The wire format is bincode-encoded bytes.
95///
96/// ```
97/// # use hermes_core::Subject;
98/// // Concrete:
99/// let s = Subject::new().str("job").int(42).str("logs");
100/// assert_eq!(s.to_string(), "job.42.logs");
101///
102/// // Pattern:
103/// let p = Subject::new().str("job").any().str("logs");
104/// assert_eq!(p.to_string(), "job.*.logs");
105///
106/// // Roundtrip through bytes:
107/// let bytes = s.to_bytes();
108/// let s2 = Subject::from_bytes(&bytes).unwrap();
109/// assert_eq!(s, s2);
110/// ```
111#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[serde(transparent)]
113pub struct Subject {
114    segments: Vec<Segment>,
115}
116
117impl Subject {
118    /// Create an empty subject.
119    pub fn new() -> Self {
120        Self {
121            segments: Vec::new(),
122        }
123    }
124
125    /// Panics if the subject already ends with [`Segment::Rest`].
126    fn assert_not_terminated(&self) {
127        assert!(
128            !matches!(self.segments.last(), Some(Segment::Rest)),
129            "cannot append a segment after Rest (>)"
130        );
131    }
132
133    /// Append a string segment.
134    ///
135    /// # Panics
136    ///
137    /// Panics if the string is empty, contains reserved characters, or if
138    /// the subject already ends with `>`.
139    pub fn str(mut self, s: impl Into<String>) -> Self {
140        self.assert_not_terminated();
141        let s = s.into();
142        assert!(
143            !s.is_empty() && !s.contains(RESERVED_CHARS),
144            "segment string must not be empty or contain reserved characters (., *, >): {s:?}"
145        );
146        self.segments.push(Segment::Str(s));
147        self
148    }
149
150    /// Append an integer segment.
151    ///
152    /// # Panics
153    ///
154    /// Panics if the subject already ends with `>`.
155    pub fn int(mut self, v: i64) -> Self {
156        self.assert_not_terminated();
157        self.segments.push(Segment::Int(v));
158        self
159    }
160
161    /// Append a single-segment wildcard (`*`).
162    ///
163    /// # Panics
164    ///
165    /// Panics if the subject already ends with `>`.
166    pub fn any(mut self) -> Self {
167        self.assert_not_terminated();
168        self.segments.push(Segment::Any);
169        self
170    }
171
172    /// Append a multi-segment wildcard (`>`). Must be the last segment.
173    ///
174    /// # Panics
175    ///
176    /// Panics if `>` already exists in the subject.
177    pub fn rest(mut self) -> Self {
178        assert!(
179            !self.segments.iter().any(|s| matches!(s, Segment::Rest)),
180            "Rest (>) must be the last segment and cannot appear more than once"
181        );
182        self.segments.push(Segment::Rest);
183        self
184    }
185
186    /// Append a raw [`Segment`].
187    ///
188    /// # Panics
189    ///
190    /// Panics if the subject already ends with `>`.
191    pub fn segment(mut self, seg: Segment) -> Self {
192        self.assert_not_terminated();
193        self.segments.push(seg);
194        self
195    }
196
197    /// Access the inner segments.
198    pub fn segments(&self) -> &[Segment] {
199        &self.segments
200    }
201
202    /// Number of segments.
203    pub fn len(&self) -> usize {
204        self.segments.len()
205    }
206
207    /// Whether the subject is empty.
208    pub fn is_empty(&self) -> bool {
209        self.segments.is_empty()
210    }
211
212    /// `true` if the subject contains no wildcards.
213    ///
214    /// Concrete subjects enable fast-path exact-match routing in the broker.
215    pub fn is_concrete(&self) -> bool {
216        self.segments
217            .iter()
218            .all(|s| matches!(s, Segment::Str(_) | Segment::Int(_)))
219    }
220
221    /// `true` if the subject contains any wildcard segment.
222    pub fn is_pattern(&self) -> bool {
223        !self.is_concrete()
224    }
225
226    /// Test whether a concrete subject matches this pattern.
227    ///
228    /// If `self` is concrete, this is an equality check.
229    pub fn matches(&self, concrete: &Subject) -> bool {
230        let pat = &self.segments;
231        let subj = &concrete.segments;
232
233        // Fast path: trailing Rest matches zero-or-more.
234        if let Some(Segment::Rest) = pat.last() {
235            let prefix = &pat[..pat.len() - 1];
236            if subj.len() < prefix.len() {
237                return false;
238            }
239            return prefix
240                .iter()
241                .zip(subj.iter())
242                .all(|(p, s)| Self::segment_matches(p, s));
243        }
244
245        // Lengths must match exactly.
246        if pat.len() != subj.len() {
247            return false;
248        }
249
250        pat.iter()
251            .zip(subj.iter())
252            .all(|(p, s)| Self::segment_matches(p, s))
253    }
254
255    /// Check if a pattern segment matches a concrete segment.
256    fn segment_matches(pattern: &Segment, concrete: &Segment) -> bool {
257        match pattern {
258            Segment::Any | Segment::Rest => true,
259            other => other == concrete,
260        }
261    }
262
263    /// Serialize to bincode bytes (wire format).
264    pub fn to_bytes(&self) -> Vec<u8> {
265        bincode::serde::encode_to_vec(self, bincode::config::standard())
266            .expect("Subject serialization cannot fail")
267    }
268
269    /// Deserialize from bincode bytes.
270    pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::DecodeError> {
271        let (val, _) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())?;
272        Ok(val)
273    }
274
275    /// Build a [`Subject`] from `module_path!()` and a type name.
276    ///
277    /// Splits the module path on `::` and appends each part as a string
278    /// segment, then appends the type name.
279    pub fn from_module_path(module_path: &str, type_name: &str) -> Self {
280        let mut subject = Self::new();
281        for part in module_path.split("::") {
282            subject.segments.push(Segment::Str(part.to_owned()));
283        }
284        subject.segments.push(Segment::Str(type_name.to_owned()));
285        subject
286    }
287}
288
289impl Default for Subject {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295impl fmt::Display for Subject {
296    /// Dot-separated representation: `job.42.logs`, `job.*.>`.
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        for (i, seg) in self.segments.iter().enumerate() {
299            if i > 0 {
300                f.write_str(".")?;
301            }
302            write!(f, "{seg}")?;
303        }
304        Ok(())
305    }
306}
307
308impl From<&str> for Subject {
309    /// Parse a dot-separated string. `*` becomes [`Segment::Any`],
310    /// `>` becomes [`Segment::Rest`], integers become [`Segment::Int`],
311    /// everything else is [`Segment::Str`].
312    fn from(s: &str) -> Self {
313        let segments = s
314            .split('.')
315            .filter(|p| !p.is_empty())
316            .map(|part| match part {
317                "*" => Segment::Any,
318                ">" => Segment::Rest,
319                _ => part
320                    .parse::<i64>()
321                    .map(Segment::Int)
322                    .unwrap_or_else(|_| Segment::Str(part.to_owned())),
323            })
324            .collect();
325        Self { segments }
326    }
327}
328
329// ---------------------------------------------------------------------------
330// Tests
331// ---------------------------------------------------------------------------
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    // -- Concrete subjects --
338
339    #[test]
340    fn subject_builder() {
341        let s = Subject::new().str("job").int(42).str("logs");
342        assert_eq!(s.segments().len(), 3);
343        assert_eq!(s.segments()[0], Segment::Str("job".into()));
344        assert_eq!(s.segments()[1], Segment::Int(42));
345        assert_eq!(s.segments()[2], Segment::Str("logs".into()));
346        assert!(s.is_concrete());
347    }
348
349    #[test]
350    fn subject_display() {
351        let s = Subject::new().str("job").int(42).str("logs");
352        assert_eq!(s.to_string(), "job.42.logs");
353    }
354
355    #[test]
356    fn subject_bytes_roundtrip() {
357        let s = Subject::new().str("job").int(42).str("logs");
358        let bytes = s.to_bytes();
359        let s2 = Subject::from_bytes(&bytes).unwrap();
360        assert_eq!(s, s2);
361    }
362
363    #[test]
364    fn subject_from_str() {
365        let s = Subject::from("job.42.logs");
366        assert_eq!(s.segments()[0], Segment::Str("job".into()));
367        assert_eq!(s.segments()[1], Segment::Int(42));
368        assert_eq!(s.segments()[2], Segment::Str("logs".into()));
369    }
370
371    #[test]
372    fn subject_from_module_path() {
373        let s = Subject::from_module_path("app::events", "ChatMessage");
374        assert_eq!(s.to_string(), "app.events.ChatMessage");
375        let roundtripped = Subject::from_bytes(&s.to_bytes()).unwrap();
376        assert_eq!(s, roundtripped);
377    }
378
379    // -- Patterns --
380
381    #[test]
382    fn pattern_exact_match() {
383        let p = Subject::new().str("job").int(42).str("logs");
384        let s = Subject::new().str("job").int(42).str("logs");
385        assert!(p.matches(&s));
386        assert!(p.is_concrete());
387    }
388
389    #[test]
390    fn pattern_exact_mismatch() {
391        let p = Subject::new().str("job").int(42).str("logs");
392        let s = Subject::new().str("job").int(99).str("logs");
393        assert!(!p.matches(&s));
394    }
395
396    #[test]
397    fn pattern_any_single() {
398        let p = Subject::new().str("job").any().str("logs");
399        assert!(p.matches(&Subject::new().str("job").int(42).str("logs")));
400        assert!(p.matches(&Subject::new().str("job").str("foo").str("logs")));
401        assert!(!p.matches(&Subject::new().str("job").str("logs")));
402        assert!(p.is_pattern());
403    }
404
405    #[test]
406    fn pattern_rest() {
407        let p = Subject::new().str("job").rest();
408        assert!(p.matches(&Subject::new().str("job")));
409        assert!(p.matches(&Subject::new().str("job").int(42)));
410        assert!(p.matches(&Subject::new().str("job").int(42).str("logs")));
411        assert!(!p.matches(&Subject::new().str("other")));
412    }
413
414    #[test]
415    fn pattern_rest_with_any() {
416        let p = Subject::new().str("a").any().rest();
417        assert!(p.matches(&Subject::new().str("a").str("b")));
418        assert!(p.matches(&Subject::new().str("a").int(1).str("c").str("d")));
419        assert!(!p.matches(&Subject::new().str("a"))); // needs at least 2 segments
420    }
421
422    #[test]
423    fn pattern_bytes_roundtrip() {
424        let p = Subject::new().str("job").any().rest();
425        let bytes = p.to_bytes();
426        let p2 = Subject::from_bytes(&bytes).unwrap();
427        assert_eq!(p, p2);
428    }
429
430    #[test]
431    fn pattern_mixed_roundtrip() {
432        let p = Subject::new().str("a").int(1).any().rest();
433        let bytes = p.to_bytes();
434        let p2 = Subject::from_bytes(&bytes).unwrap();
435        assert_eq!(p, p2);
436    }
437
438    #[test]
439    fn pattern_display() {
440        let p = Subject::new().str("job").any().rest();
441        assert_eq!(p.to_string(), "job.*.>");
442    }
443
444    #[test]
445    fn from_str_wildcards() {
446        let s = Subject::from("job.*.>");
447        assert_eq!(s.segments()[0], Segment::Str("job".into()));
448        assert_eq!(s.segments()[1], Segment::Any);
449        assert_eq!(s.segments()[2], Segment::Rest);
450        assert!(s.is_pattern());
451    }
452
453    #[test]
454    fn concrete_is_not_pattern() {
455        let s = Subject::new().str("a").int(1);
456        assert!(s.is_concrete());
457        assert!(!s.is_pattern());
458    }
459
460    #[test]
461    #[should_panic(expected = "cannot append")]
462    fn rest_must_be_last() {
463        Subject::new().str("a").rest().str("b");
464    }
465}