Skip to main content

altium_format/query/
pattern.rs

1//! Glob-style pattern matching for selectors.
2//!
3//! Supports:
4//! - `*` - Match any characters (zero or more)
5//! - `?` - Match exactly one character
6//! - `[abc]` - Match any character in the set
7//! - `[a-z]` - Match any character in the range
8//! - `[!abc]` - Match any character NOT in the set
9
10use regex::Regex;
11use std::fmt;
12
13use crate::error::{AltiumError, Result};
14
15/// A compiled glob-style pattern for matching strings.
16#[derive(Clone)]
17pub struct Pattern {
18    /// Original pattern string
19    raw: String,
20    /// Compiled regex for matching
21    regex: Regex,
22    /// Whether this pattern is a literal (no wildcards)
23    is_literal: bool,
24}
25
26impl Pattern {
27    /// Create a new pattern from a glob string.
28    ///
29    /// # Examples
30    ///
31    /// ```
32    /// use altium_format::query::Pattern;
33    ///
34    /// let p = Pattern::new("R*").unwrap();
35    /// assert!(p.matches("R1"));
36    /// assert!(p.matches("R100"));
37    /// assert!(!p.matches("C1"));
38    ///
39    /// let p = Pattern::new("R?").unwrap();
40    /// assert!(p.matches("R1"));
41    /// assert!(!p.matches("R10"));
42    /// ```
43    pub fn new(pattern: &str) -> Result<Self> {
44        let is_literal = !pattern.contains(['*', '?', '[']);
45        let regex_str = if is_literal {
46            format!("(?i)^{}$", regex::escape(pattern))
47        } else {
48            Self::glob_to_regex(pattern)?
49        };
50
51        let regex = Regex::new(&regex_str)
52            .map_err(|e| AltiumError::Parse(format!("Invalid pattern '{}': {}", pattern, e)))?;
53
54        Ok(Self {
55            raw: pattern.to_string(),
56            regex,
57            is_literal,
58        })
59    }
60
61    /// Create a pattern that matches a literal string exactly.
62    pub fn literal(s: &str) -> Self {
63        let regex_str = format!("(?i)^{}$", regex::escape(s));
64        Self {
65            raw: s.to_string(),
66            regex: Regex::new(&regex_str).unwrap(),
67            is_literal: true,
68        }
69    }
70
71    /// Create a pattern that matches anything.
72    pub fn any() -> Self {
73        Self {
74            raw: "*".to_string(),
75            regex: Regex::new(".*").unwrap(),
76            is_literal: false,
77        }
78    }
79
80    /// Returns the original pattern string.
81    pub fn as_str(&self) -> &str {
82        &self.raw
83    }
84
85    /// Returns true if this pattern has no wildcards.
86    pub fn is_literal(&self) -> bool {
87        self.is_literal
88    }
89
90    /// Test if a string matches this pattern (case-insensitive).
91    pub fn matches(&self, text: &str) -> bool {
92        self.regex.is_match(text)
93    }
94
95    /// Test if a string matches this pattern (case-sensitive).
96    pub fn matches_case_sensitive(&self, text: &str) -> bool {
97        if self.is_literal {
98            self.raw == text
99        } else {
100            // For case-sensitive, we'd need a separate regex
101            // For now, fall back to case-insensitive
102            self.regex.is_match(text)
103        }
104    }
105
106    /// Convert a glob pattern to a regex string.
107    fn glob_to_regex(pattern: &str) -> Result<String> {
108        let mut result = String::from("(?i)^"); // Case-insensitive
109        let mut chars = pattern.chars().peekable();
110
111        while let Some(c) = chars.next() {
112            match c {
113                '*' => result.push_str(".*"),
114                '?' => result.push('.'),
115                '[' => {
116                    result.push('[');
117                    // Handle negation [!...]
118                    if chars.peek() == Some(&'!') {
119                        chars.next();
120                        result.push('^');
121                    }
122                    // Copy characters until closing ]
123                    // Don't escape '-' as it's used for ranges like [1-4]
124                    let mut found_close = false;
125                    for c in chars.by_ref() {
126                        if c == ']' {
127                            result.push(']');
128                            found_close = true;
129                            break;
130                        }
131                        // Only escape backslash and caret inside character class
132                        if c == '\\' || c == '^' {
133                            result.push('\\');
134                        }
135                        result.push(c);
136                    }
137                    if !found_close {
138                        return Err(AltiumError::Parse(format!(
139                            "Unclosed '[' in pattern '{}'",
140                            pattern
141                        )));
142                    }
143                }
144                // Escape regex metacharacters
145                '.' | '+' | '^' | '$' | '(' | ')' | '{' | '}' | '|' | '\\' => {
146                    result.push('\\');
147                    result.push(c);
148                }
149                _ => result.push(c),
150            }
151        }
152
153        result.push('$');
154        Ok(result)
155    }
156}
157
158impl fmt::Debug for Pattern {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        f.debug_struct("Pattern")
161            .field("raw", &self.raw)
162            .field("is_literal", &self.is_literal)
163            .finish()
164    }
165}
166
167impl fmt::Display for Pattern {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        write!(f, "{}", self.raw)
170    }
171}
172
173impl PartialEq for Pattern {
174    fn eq(&self, other: &Self) -> bool {
175        self.raw == other.raw
176    }
177}
178
179impl Eq for Pattern {}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_literal_pattern() {
187        let p = Pattern::new("R1").unwrap();
188        assert!(p.is_literal());
189        assert!(p.matches("R1"));
190        assert!(p.matches("r1")); // Case-insensitive
191        assert!(!p.matches("R2"));
192        assert!(!p.matches("R10"));
193    }
194
195    #[test]
196    fn test_star_wildcard() {
197        let p = Pattern::new("R*").unwrap();
198        assert!(!p.is_literal());
199        assert!(p.matches("R"));
200        assert!(p.matches("R1"));
201        assert!(p.matches("R10"));
202        assert!(p.matches("R100"));
203        assert!(p.matches("RESISTOR"));
204        assert!(!p.matches("C1"));
205    }
206
207    #[test]
208    fn test_question_wildcard() {
209        let p = Pattern::new("R?").unwrap();
210        assert!(p.matches("R1"));
211        assert!(p.matches("R9"));
212        assert!(p.matches("Ra"));
213        assert!(!p.matches("R"));
214        assert!(!p.matches("R10"));
215    }
216
217    #[test]
218    fn test_double_question() {
219        let p = Pattern::new("R??").unwrap();
220        assert!(p.matches("R10"));
221        assert!(p.matches("R99"));
222        assert!(!p.matches("R1"));
223        assert!(!p.matches("R100"));
224    }
225
226    #[test]
227    fn test_character_class() {
228        let p = Pattern::new("[RC]*").unwrap();
229        assert!(p.matches("R1"));
230        assert!(p.matches("C1"));
231        assert!(p.matches("R100"));
232        assert!(!p.matches("U1"));
233        assert!(!p.matches("L1"));
234    }
235
236    #[test]
237    fn test_character_range() {
238        let p = Pattern::new("U[1-4]").unwrap();
239        assert!(p.matches("U1"));
240        assert!(p.matches("U2"));
241        assert!(p.matches("U3"));
242        assert!(p.matches("U4"));
243        assert!(!p.matches("U5"));
244        assert!(!p.matches("U0"));
245    }
246
247    #[test]
248    fn test_negated_class() {
249        let p = Pattern::new("[!RC]*").unwrap();
250        assert!(p.matches("U1"));
251        assert!(p.matches("L1"));
252        assert!(!p.matches("R1"));
253        assert!(!p.matches("C1"));
254    }
255
256    #[test]
257    fn test_complex_pattern() {
258        let p = Pattern::new("*CLK*").unwrap();
259        assert!(p.matches("CLK"));
260        assert!(p.matches("SPI_CLK"));
261        assert!(p.matches("CLK_OUT"));
262        assert!(p.matches("SYS_CLK_IN"));
263        assert!(!p.matches("CLOCK"));
264    }
265
266    #[test]
267    fn test_case_insensitive() {
268        let p = Pattern::new("VCC").unwrap();
269        assert!(p.matches("VCC"));
270        assert!(p.matches("vcc"));
271        assert!(p.matches("Vcc"));
272    }
273
274    #[test]
275    fn test_special_chars() {
276        let p = Pattern::new("R1.5K").unwrap();
277        assert!(p.matches("R1.5K"));
278        assert!(!p.matches("R15K"));
279    }
280}