1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct Topic(String);
13
14impl Topic {
15 pub fn new(topic: impl Into<String>) -> Self {
17 Self(topic.into())
18 }
19
20 pub fn as_str(&self) -> &str {
22 &self.0
23 }
24
25 pub fn is_global_wildcard(&self) -> bool {
30 self.0 == "*"
31 }
32
33 pub fn matches(&self, topic: &Topic) -> bool {
40 self.matches_str(topic.as_str())
41 }
42
43 pub fn matches_str(&self, target: &str) -> bool {
48 let pattern = &self.0;
49
50 if pattern == "*" {
52 return true;
53 }
54
55 if pattern == target {
57 return true;
58 }
59
60 if !pattern.contains('*') {
62 return false;
63 }
64
65 let mut pattern_parts = pattern.split('.');
67 let mut target_parts = target.split('.');
68
69 loop {
70 match (pattern_parts.next(), target_parts.next()) {
71 (Some(p), Some(t)) => {
72 if p != "*" && p != t {
73 return false;
74 }
75 }
76 (None, None) => return true,
77 _ => return false, }
79 }
80 }
81}
82
83impl From<&str> for Topic {
84 fn from(s: &str) -> Self {
85 Self::new(s)
86 }
87}
88
89impl From<String> for Topic {
90 fn from(s: String) -> Self {
91 Self::new(s)
92 }
93}
94
95impl std::fmt::Display for Topic {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 write!(f, "{}", self.0)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn test_exact_match() {
107 let pattern = Topic::new("impl.done");
108 let target = Topic::new("impl.done");
109 assert!(pattern.matches(&target));
110 }
111
112 #[test]
113 fn test_no_match() {
114 let pattern = Topic::new("impl.done");
115 let target = Topic::new("review.done");
116 assert!(!pattern.matches(&target));
117 }
118
119 #[test]
120 fn test_wildcard_suffix() {
121 let pattern = Topic::new("impl.*");
122 assert!(pattern.matches(&Topic::new("impl.done")));
123 assert!(pattern.matches(&Topic::new("impl.started")));
124 assert!(!pattern.matches(&Topic::new("review.done")));
125 }
126
127 #[test]
128 fn test_wildcard_prefix() {
129 let pattern = Topic::new("*.done");
130 assert!(pattern.matches(&Topic::new("impl.done")));
131 assert!(pattern.matches(&Topic::new("review.done")));
132 assert!(!pattern.matches(&Topic::new("impl.started")));
133 }
134
135 #[test]
136 fn test_global_wildcard() {
137 let pattern = Topic::new("*");
138 assert!(pattern.matches(&Topic::new("impl.done")));
139 assert!(pattern.matches(&Topic::new("anything")));
140 }
141
142 #[test]
143 fn test_length_mismatch() {
144 let pattern = Topic::new("impl.*");
145 assert!(!pattern.matches(&Topic::new("impl.sub.done")));
146 }
147}