Skip to main content

anycms_event/
topic.rs

1//! Topic pattern matching for event routing.
2//!
3//! Supports wildcard patterns for flexible event subscription:
4//! - `*` matches a single segment (e.g., `"user.*"` matches `"user.created"` but not `"user.foo.bar"`)
5//! - `**` matches multiple segments (e.g., `"user.**"` matches `"user.created"` and `"user.foo.bar"`)
6//! - Exact match when no wildcards are present
7
8/// Match a topic pattern against a concrete topic name.
9///
10/// # Examples
11///
12/// ```ignore
13/// use anycms_event::topic::matches;
14///
15/// assert!(matches("user.created", "user.created"));
16/// assert!(matches("user.*", "user.created"));
17/// assert!(matches("user.**", "user.foo.bar"));
18/// assert!(matches("**", "anything.at.all"));
19/// assert!(!matches("user.*", "user.foo.bar"));
20/// ```
21pub fn matches(pattern: &str, topic: &str) -> bool {
22    let pattern_parts: Vec<&str> = pattern.split('.').collect();
23    let topic_parts: Vec<&str> = topic.split('.').collect();
24    match_glob(&pattern_parts, &topic_parts)
25}
26
27fn match_glob(pattern: &[&str], topic: &[&str]) -> bool {
28    match (pattern.first(), topic.first()) {
29        (None, None) => true,
30        (None, Some(_)) => false,
31        (Some(&"**"), _) => {
32            // ** matches zero or more segments
33            if pattern.len() == 1 {
34                return true;
35            }
36            // Try matching rest of pattern against current and remaining topic positions
37            (0..=topic.len()).any(|i| match_glob(&pattern[1..], &topic[i..]))
38        }
39        (Some(&"*"), None) => false,
40        (Some(&"*"), Some(_)) => match_glob(&pattern[1..], &topic[1..]),
41        (Some(p), Some(t)) if *p == *t => match_glob(&pattern[1..], &topic[1..]),
42        _ => false,
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn test_exact_match() {
52        assert!(matches("user.created", "user.created"));
53        assert!(!matches("user.created", "user.deleted"));
54    }
55
56    #[test]
57    fn test_single_wildcard() {
58        assert!(matches("user.*", "user.created"));
59        assert!(matches("user.*", "user.deleted"));
60        assert!(!matches("user.*", "user.foo.bar"));
61        assert!(matches("*.created", "user.created"));
62        assert!(!matches("*.created", "user.deleted"));
63    }
64
65    #[test]
66    fn test_double_wildcard() {
67        assert!(matches("user.**", "user.created"));
68        assert!(matches("user.**", "user.foo.bar"));
69        assert!(matches("**", "anything.at.all"));
70        assert!(matches("*.**", "user.foo.bar"));
71        assert!(matches("user.**", "user"));
72    }
73
74    #[test]
75    fn test_empty_patterns() {
76        assert!(matches("", ""));
77        assert!(!matches("", "something"));
78        assert!(!matches("something", ""));
79    }
80
81    #[test]
82    fn test_multiple_wildcards() {
83        assert!(matches("*.created.*", "user.created.today"));
84        assert!(!matches("*.created.*", "user.created"));
85        assert!(matches("*.*.*", "a.b.c"));
86        assert!(!matches("*.*.*", "a.b"));
87    }
88}