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() {
137 return false;
138 } else if !match_single(&addr[ai], pat) {
139 return false;
140 }
141
142 ai += 1;
143 pi += 1;
144 }
145
146 ai == addr.len()
147}
148
149fn match_single(segment: &str, pattern: &str) -> bool {
151 if pattern == "*" {
152 true
153 } else {
154 segment == pattern
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct Pattern {
161 address: Address,
162 regex: Option<regex_lite::Regex>,
163}
164
165impl Pattern {
166 pub fn compile(s: &str) -> Result<Self> {
168 let address = Address::parse(s)?;
169
170 let regex = if address.is_pattern() {
173 let regex_str = s
177 .replace("/**", "§§") .replace("/**/", "§§/") .replace('*', "[^/]*") .replace("§§", "(/[^/]+)*"); let regex_str = format!("^{}$", regex_str);
182 Some(
183 regex_lite::Regex::new(®ex_str)
184 .map_err(|e| Error::InvalidPattern(e.to_string()))?,
185 )
186 } else {
187 None
188 };
189
190 Ok(Self { address, regex })
191 }
192
193 pub fn matches(&self, addr: &str) -> bool {
196 if self.address.is_pattern() {
197 glob_match::glob_match(self.address.as_str(), addr)
199 } else {
200 addr == self.address.as_str()
202 }
203 }
204
205 pub fn matches_address(&self, addr: &Address) -> bool {
207 self.matches(addr.as_str())
208 }
209
210 pub fn address(&self) -> &Address {
212 &self.address
213 }
214}
215
216pub fn glob_match(pattern: &str, address: &str) -> bool {
218 glob_match::glob_match(pattern, address)
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_parse_valid() {
227 let addr = Address::parse("/lumen/scene/0/layer/3/opacity").unwrap();
228 assert_eq!(addr.segments().len(), 6);
229 assert_eq!(addr.namespace(), Some("lumen"));
230 assert_eq!(addr.property(), Some("opacity"));
231 }
232
233 #[test]
234 fn test_parse_invalid() {
235 assert!(Address::parse("").is_err());
236 assert!(Address::parse("no/leading/slash").is_err());
237 }
238
239 #[test]
240 fn test_single_wildcard() {
241 let pattern = Pattern::compile("/lumen/scene/*/layer/*/opacity").unwrap();
242
243 assert!(pattern.matches("/lumen/scene/0/layer/3/opacity"));
244 assert!(pattern.matches("/lumen/scene/1/layer/0/opacity"));
245 assert!(!pattern.matches("/lumen/scene/0/layer/3/color"));
246 assert!(!pattern.matches("/lumen/scene/opacity"));
247 }
248
249 #[test]
250 fn test_double_wildcard() {
251 let pattern = Pattern::compile("/lumen/**/opacity").unwrap();
252
253 assert!(pattern.matches("/lumen/scene/0/opacity"));
254 assert!(pattern.matches("/lumen/scene/0/layer/3/opacity"));
255 assert!(pattern.matches("/lumen/opacity"));
256 assert!(!pattern.matches("/lumen/scene/0/color"));
257 }
258
259 #[test]
260 fn test_exact_match() {
261 let pattern = Pattern::compile("/lumen/scene/0/opacity").unwrap();
262
263 assert!(pattern.matches("/lumen/scene/0/opacity"));
264 assert!(!pattern.matches("/lumen/scene/1/opacity"));
265 }
266
267 #[test]
268 fn test_glob_match_fn() {
269 assert!(glob_match("/lumen/**", "/lumen/scene/0/opacity"));
270 assert!(glob_match("/lumen/*/opacity", "/lumen/scene/opacity"));
271 assert!(!glob_match("/lumen/*/opacity", "/lumen/scene/0/opacity"));
272 }
273}