1use crate::{Error, Result};
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct Address {
19 raw: String,
20 segments: Vec<String>,
21}
22
23impl Address {
24 pub fn parse(s: &str) -> Result<Self> {
26 if s.is_empty() {
27 return Err(Error::InvalidAddress("empty address".to_string()));
28 }
29
30 if !s.starts_with('/') {
31 return Err(Error::InvalidAddress(format!(
32 "address must start with '/': {}",
33 s
34 )));
35 }
36
37 let segments: Vec<String> = s[1..]
38 .split('/')
39 .map(|s| s.to_string())
40 .collect();
41
42 for (i, seg) in segments.iter().enumerate() {
44 if seg.is_empty() && i < segments.len() - 1 {
45 return Err(Error::InvalidAddress(format!(
46 "empty segment in address: {}",
47 s
48 )));
49 }
50 }
51
52 Ok(Self {
53 raw: s.to_string(),
54 segments,
55 })
56 }
57
58 pub fn as_str(&self) -> &str {
60 &self.raw
61 }
62
63 pub fn segments(&self) -> &[String] {
65 &self.segments
66 }
67
68 pub fn namespace(&self) -> Option<&str> {
70 self.segments.first().map(|s| s.as_str())
71 }
72
73 pub fn property(&self) -> Option<&str> {
75 self.segments.last().map(|s| s.as_str())
76 }
77
78 pub fn is_pattern(&self) -> bool {
80 self.segments.iter().any(|s| s == "*" || s == "**")
81 }
82
83 pub fn matches(&self, pattern: &Address) -> bool {
85 match_segments(&self.segments, &pattern.segments)
86 }
87}
88
89impl std::fmt::Display for Address {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 write!(f, "{}", self.raw)
92 }
93}
94
95impl TryFrom<&str> for Address {
96 type Error = Error;
97
98 fn try_from(s: &str) -> Result<Self> {
99 Address::parse(s)
100 }
101}
102
103impl TryFrom<String> for Address {
104 type Error = Error;
105
106 fn try_from(s: String) -> Result<Self> {
107 Address::parse(&s)
108 }
109}
110
111fn match_segments(addr: &[String], pattern: &[String]) -> bool {
113 let mut ai = 0;
114 let mut pi = 0;
115
116 while pi < pattern.len() {
117 let pat = &pattern[pi];
118
119 if pat == "**" {
120 if pi == pattern.len() - 1 {
122 return true;
124 }
125
126 let next_pat = &pattern[pi + 1];
128 while ai < addr.len() {
129 if match_single(&addr[ai], next_pat) {
130 if match_segments(&addr[ai..], &pattern[pi + 1..]) {
132 return true;
133 }
134 }
135 ai += 1;
136 }
137 return false;
138 } else if ai >= addr.len() {
139 return false;
140 } else if !match_single(&addr[ai], pat) {
141 return false;
142 }
143
144 ai += 1;
145 pi += 1;
146 }
147
148 ai == addr.len()
149}
150
151fn match_single(segment: &str, pattern: &str) -> bool {
153 if pattern == "*" {
154 true
155 } else {
156 segment == pattern
157 }
158}
159
160#[derive(Debug, Clone)]
162pub struct Pattern {
163 address: Address,
164 regex: Option<regex_lite::Regex>,
165}
166
167impl Pattern {
168 pub fn compile(s: &str) -> Result<Self> {
170 let address = Address::parse(s)?;
171
172 let regex = if address.is_pattern() {
174 let regex_str = s
177 .replace("/**", "§§") .replace('*', "[^/]+")
179 .replace("§§", "(/[^/]+)*"); let regex_str = format!("^{}$", regex_str);
181 Some(
182 regex_lite::Regex::new(®ex_str)
183 .map_err(|e| Error::InvalidPattern(e.to_string()))?,
184 )
185 } else {
186 None
187 };
188
189 Ok(Self { address, regex })
190 }
191
192 pub fn matches(&self, addr: &str) -> bool {
194 if let Some(regex) = &self.regex {
195 regex.is_match(addr)
196 } else {
197 addr == self.address.as_str()
198 }
199 }
200
201 pub fn matches_address(&self, addr: &Address) -> bool {
203 self.matches(addr.as_str())
204 }
205
206 pub fn address(&self) -> &Address {
208 &self.address
209 }
210}
211
212pub fn glob_match(pattern: &str, address: &str) -> bool {
214 glob_match::glob_match(pattern, address)
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_parse_valid() {
223 let addr = Address::parse("/lumen/scene/0/layer/3/opacity").unwrap();
224 assert_eq!(addr.segments().len(), 6);
225 assert_eq!(addr.namespace(), Some("lumen"));
226 assert_eq!(addr.property(), Some("opacity"));
227 }
228
229 #[test]
230 fn test_parse_invalid() {
231 assert!(Address::parse("").is_err());
232 assert!(Address::parse("no/leading/slash").is_err());
233 }
234
235 #[test]
236 fn test_single_wildcard() {
237 let pattern = Pattern::compile("/lumen/scene/*/layer/*/opacity").unwrap();
238
239 assert!(pattern.matches("/lumen/scene/0/layer/3/opacity"));
240 assert!(pattern.matches("/lumen/scene/1/layer/0/opacity"));
241 assert!(!pattern.matches("/lumen/scene/0/layer/3/color"));
242 assert!(!pattern.matches("/lumen/scene/opacity"));
243 }
244
245 #[test]
246 fn test_double_wildcard() {
247 let pattern = Pattern::compile("/lumen/**/opacity").unwrap();
248
249 assert!(pattern.matches("/lumen/scene/0/opacity"));
250 assert!(pattern.matches("/lumen/scene/0/layer/3/opacity"));
251 assert!(pattern.matches("/lumen/opacity"));
252 assert!(!pattern.matches("/lumen/scene/0/color"));
253 }
254
255 #[test]
256 fn test_exact_match() {
257 let pattern = Pattern::compile("/lumen/scene/0/opacity").unwrap();
258
259 assert!(pattern.matches("/lumen/scene/0/opacity"));
260 assert!(!pattern.matches("/lumen/scene/1/opacity"));
261 }
262
263 #[test]
264 fn test_glob_match_fn() {
265 assert!(glob_match("/lumen/**", "/lumen/scene/0/opacity"));
266 assert!(glob_match("/lumen/*/opacity", "/lumen/scene/opacity"));
267 assert!(!glob_match("/lumen/*/opacity", "/lumen/scene/0/opacity"));
268 }
269}