altium_format/query/
pattern.rs1use regex::Regex;
11use std::fmt;
12
13use crate::error::{AltiumError, Result};
14
15#[derive(Clone)]
17pub struct Pattern {
18 raw: String,
20 regex: Regex,
22 is_literal: bool,
24}
25
26impl Pattern {
27 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(®ex_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 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(®ex_str).unwrap(),
67 is_literal: true,
68 }
69 }
70
71 pub fn any() -> Self {
73 Self {
74 raw: "*".to_string(),
75 regex: Regex::new(".*").unwrap(),
76 is_literal: false,
77 }
78 }
79
80 pub fn as_str(&self) -> &str {
82 &self.raw
83 }
84
85 pub fn is_literal(&self) -> bool {
87 self.is_literal
88 }
89
90 pub fn matches(&self, text: &str) -> bool {
92 self.regex.is_match(text)
93 }
94
95 pub fn matches_case_sensitive(&self, text: &str) -> bool {
97 if self.is_literal {
98 self.raw == text
99 } else {
100 self.regex.is_match(text)
103 }
104 }
105
106 fn glob_to_regex(pattern: &str) -> Result<String> {
108 let mut result = String::from("(?i)^"); 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 if chars.peek() == Some(&'!') {
119 chars.next();
120 result.push('^');
121 }
122 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 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 '.' | '+' | '^' | '$' | '(' | ')' | '{' | '}' | '|' | '\\' => {
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")); 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}