clasp_core/
address.rs

1//! Address parsing and pattern matching
2//!
3//! Clasp addresses follow this format:
4//! ```text
5//! /namespace/category/instance/property
6//! /lumen/scene/0/layer/3/opacity
7//! /midi/launchpad/cc/74
8//! ```
9//!
10//! Wildcards (for subscriptions):
11//! - `*` matches one segment
12//! - `**` matches any number of segments
13
14use crate::{Error, Result};
15
16/// A parsed Clasp address
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct Address {
19    raw: String,
20    segments: Vec<String>,
21}
22
23impl Address {
24    /// Parse an address string
25    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        // Validate segments
43        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    /// Get the raw address string
59    pub fn as_str(&self) -> &str {
60        &self.raw
61    }
62
63    /// Get the address segments
64    pub fn segments(&self) -> &[String] {
65        &self.segments
66    }
67
68    /// Get the namespace (first segment)
69    pub fn namespace(&self) -> Option<&str> {
70        self.segments.first().map(|s| s.as_str())
71    }
72
73    /// Get the last segment (usually the property name)
74    pub fn property(&self) -> Option<&str> {
75        self.segments.last().map(|s| s.as_str())
76    }
77
78    /// Check if this address contains wildcards
79    pub fn is_pattern(&self) -> bool {
80        self.segments.iter().any(|s| s == "*" || s == "**")
81    }
82
83    /// Check if this address matches a pattern
84    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
111/// Match address segments against pattern segments
112fn 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            // ** matches zero or more segments
121            if pi == pattern.len() - 1 {
122                // ** at end matches everything
123                return true;
124            }
125
126            // Try to match remaining pattern after **
127            let next_pat = &pattern[pi + 1];
128            while ai < addr.len() {
129                if match_single(&addr[ai], next_pat) {
130                    // Try matching rest of pattern
131                    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
151/// Match a single segment against a pattern segment
152fn match_single(segment: &str, pattern: &str) -> bool {
153    if pattern == "*" {
154        true
155    } else {
156        segment == pattern
157    }
158}
159
160/// A compiled pattern for efficient matching
161#[derive(Debug, Clone)]
162pub struct Pattern {
163    address: Address,
164    regex: Option<regex_lite::Regex>,
165}
166
167impl Pattern {
168    /// Compile a pattern from an address string
169    pub fn compile(s: &str) -> Result<Self> {
170        let address = Address::parse(s)?;
171
172        // Build regex for efficient matching
173        let regex = if address.is_pattern() {
174            // ** matches zero or more path segments (including slashes)
175            // * matches exactly one path segment (no slashes)
176            let regex_str = s
177                .replace("/**", "§§") // Temp placeholder for /**/
178                .replace('*', "[^/]+")
179                .replace("§§", "(/[^/]+)*"); // Match zero or more /segment
180            let regex_str = format!("^{}$", regex_str);
181            Some(
182                regex_lite::Regex::new(&regex_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    /// Check if an address matches this pattern
193    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    /// Check if an Address matches this pattern
202    pub fn matches_address(&self, addr: &Address) -> bool {
203        self.matches(addr.as_str())
204    }
205
206    /// Get the underlying address
207    pub fn address(&self) -> &Address {
208        &self.address
209    }
210}
211
212// Use glob-match for simple cases
213pub 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}