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..].split('/').map(|s| s.to_string()).collect();
38
39        // Validate segments
40        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    /// Get the raw address string
56    pub fn as_str(&self) -> &str {
57        &self.raw
58    }
59
60    /// Get the address segments
61    pub fn segments(&self) -> &[String] {
62        &self.segments
63    }
64
65    /// Get the namespace (first segment)
66    pub fn namespace(&self) -> Option<&str> {
67        self.segments.first().map(|s| s.as_str())
68    }
69
70    /// Get the last segment (usually the property name)
71    pub fn property(&self) -> Option<&str> {
72        self.segments.last().map(|s| s.as_str())
73    }
74
75    /// Check if this address contains wildcards
76    pub fn is_pattern(&self) -> bool {
77        self.segments.iter().any(|s| s == "*" || s == "**")
78    }
79
80    /// Check if this address matches a pattern
81    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
108/// Match address segments against pattern segments
109fn 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            // ** matches zero or more segments
118            if pi == pattern.len() - 1 {
119                // ** at end matches everything
120                return true;
121            }
122
123            // Try to match remaining pattern after **
124            let next_pat = &pattern[pi + 1];
125            while ai < addr.len() {
126                if match_single(&addr[ai], next_pat) {
127                    // Try matching rest of pattern
128                    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
148/// Match a single segment against a pattern segment
149fn match_single(segment: &str, pattern: &str) -> bool {
150    if pattern == "*" {
151        true
152    } else {
153        segment == pattern
154    }
155}
156
157/// A compiled pattern for efficient matching
158#[derive(Debug, Clone)]
159pub struct Pattern {
160    address: Address,
161    regex: Option<regex_lite::Regex>,
162}
163
164impl Pattern {
165    /// Compile a pattern from an address string
166    pub fn compile(s: &str) -> Result<Self> {
167        let address = Address::parse(s)?;
168
169        // Build regex for efficient matching
170        let regex = if address.is_pattern() {
171            // ** matches zero or more path segments (including slashes)
172            // * matches exactly one path segment (no slashes)
173            let regex_str = s
174                .replace("/**", "§§") // Temp placeholder for /**/
175                .replace('*', "[^/]+")
176                .replace("§§", "(/[^/]+)*"); // Match zero or more /segment
177            let regex_str = format!("^{}$", regex_str);
178            Some(
179                regex_lite::Regex::new(&regex_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    /// Check if an address matches this pattern
190    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    /// Check if an Address matches this pattern
199    pub fn matches_address(&self, addr: &Address) -> bool {
200        self.matches(addr.as_str())
201    }
202
203    /// Get the underlying address
204    pub fn address(&self) -> &Address {
205        &self.address
206    }
207}
208
209// Use glob-match for simple cases
210pub 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}