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 {
78 self.segments.iter().any(|s| s.contains('*'))
79 }
80
81 pub fn matches(&self, pattern: &Address) -> bool {
83 match_segments(&self.segments, &pattern.segments)
84 }
85}
86
87impl std::fmt::Display for Address {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(f, "{}", self.raw)
90 }
91}
92
93impl TryFrom<&str> for Address {
94 type Error = Error;
95
96 fn try_from(s: &str) -> Result<Self> {
97 Address::parse(s)
98 }
99}
100
101impl TryFrom<String> for Address {
102 type Error = Error;
103
104 fn try_from(s: String) -> Result<Self> {
105 Address::parse(&s)
106 }
107}
108
109fn match_segments(addr: &[String], pattern: &[String]) -> bool {
111 let mut ai = 0;
112 let mut pi = 0;
113
114 while pi < pattern.len() {
115 let pat = &pattern[pi];
116
117 if pat == "**" {
118 if pi == pattern.len() - 1 {
120 return true;
122 }
123
124 let next_pat = &pattern[pi + 1];
126 while ai < addr.len() {
127 if match_single(&addr[ai], next_pat) {
128 if match_segments(&addr[ai..], &pattern[pi + 1..]) {
130 return true;
131 }
132 }
133 ai += 1;
134 }
135 return false;
136 } else if ai >= addr.len() || !match_single(&addr[ai], pat) {
137 return false;
138 }
139
140 ai += 1;
141 pi += 1;
142 }
143
144 ai == addr.len()
145}
146
147fn match_single(segment: &str, pattern: &str) -> bool {
149 if pattern == "*" {
150 true
151 } else {
152 segment == pattern
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct Pattern {
159 address: Address,
160 _regex: Option<regex_lite::Regex>,
161}
162
163impl Pattern {
164 pub fn compile(s: &str) -> Result<Self> {
166 let address = Address::parse(s)?;
167
168 let regex = if address.is_pattern() {
171 let regex_str = s
175 .replace("/**", "§§") .replace("/**/", "§§/") .replace('*', "[^/]*") .replace("§§", "(/[^/]+)*"); let regex_str = format!("^{}$", regex_str);
180 Some(
181 regex_lite::Regex::new(®ex_str)
182 .map_err(|e| Error::InvalidPattern(e.to_string()))?,
183 )
184 } else {
185 None
186 };
187
188 Ok(Self {
189 address,
190 _regex: regex,
191 })
192 }
193
194 pub fn matches(&self, addr: &str) -> bool {
197 if self.address.is_pattern() {
198 glob_match::glob_match(self.address.as_str(), addr)
200 } else {
201 addr == self.address.as_str()
203 }
204 }
205
206 pub fn matches_address(&self, addr: &Address) -> bool {
208 self.matches(addr.as_str())
209 }
210
211 pub fn address(&self) -> &Address {
213 &self.address
214 }
215}
216
217pub fn glob_match(pattern: &str, address: &str) -> bool {
219 glob_match::glob_match(pattern, address)
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_parse_valid() {
228 let addr = Address::parse("/lumen/scene/0/layer/3/opacity").unwrap();
229 assert_eq!(addr.segments().len(), 6);
230 assert_eq!(addr.namespace(), Some("lumen"));
231 assert_eq!(addr.property(), Some("opacity"));
232 }
233
234 #[test]
235 fn test_parse_invalid() {
236 assert!(Address::parse("").is_err());
237 assert!(Address::parse("no/leading/slash").is_err());
238 }
239
240 #[test]
241 fn test_single_wildcard() {
242 let pattern = Pattern::compile("/lumen/scene/*/layer/*/opacity").unwrap();
243
244 assert!(pattern.matches("/lumen/scene/0/layer/3/opacity"));
245 assert!(pattern.matches("/lumen/scene/1/layer/0/opacity"));
246 assert!(!pattern.matches("/lumen/scene/0/layer/3/color"));
247 assert!(!pattern.matches("/lumen/scene/opacity"));
248 }
249
250 #[test]
251 fn test_double_wildcard() {
252 let pattern = Pattern::compile("/lumen/**/opacity").unwrap();
253
254 assert!(pattern.matches("/lumen/scene/0/opacity"));
255 assert!(pattern.matches("/lumen/scene/0/layer/3/opacity"));
256 assert!(pattern.matches("/lumen/opacity"));
257 assert!(!pattern.matches("/lumen/scene/0/color"));
258 }
259
260 #[test]
261 fn test_exact_match() {
262 let pattern = Pattern::compile("/lumen/scene/0/opacity").unwrap();
263
264 assert!(pattern.matches("/lumen/scene/0/opacity"));
265 assert!(!pattern.matches("/lumen/scene/1/opacity"));
266 }
267
268 #[test]
269 fn test_glob_match_fn() {
270 assert!(glob_match("/lumen/**", "/lumen/scene/0/opacity"));
271 assert!(glob_match("/lumen/*/opacity", "/lumen/scene/opacity"));
272 assert!(!glob_match("/lumen/*/opacity", "/lumen/scene/0/opacity"));
273 }
274}