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