1use crate::SingleLineMatch;
7use crate::{Match, Origin, Problem};
8use regex::{Captures, Regex};
9use std::fmt::Display;
10
11pub type MatchResult = Result<Option<(Box<dyn Match>, Option<Box<dyn Problem>>)>, Error>;
13
14pub type RegexCallback =
16 Box<dyn Fn(&Captures) -> Result<Option<Box<dyn Problem>>, Error> + Send + Sync>;
17
18#[derive(Debug)]
22pub struct Error {
23 pub message: String,
25}
26
27impl Display for Error {
28 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
29 self.message.fmt(f)
30 }
31}
32
33impl std::error::Error for Error {}
34
35pub struct RegexLineMatcher {
40 regex: Regex,
42 callback: RegexCallback,
44}
45
46pub trait Matcher: Sync {
51 fn extract_from_lines(&self, lines: &[&str], offset: usize) -> MatchResult;
62}
63
64impl RegexLineMatcher {
65 pub fn new(regex: Regex, callback: RegexCallback) -> Self {
74 Self { regex, callback }
75 }
76
77 pub fn matches_line(&self, line: &str) -> bool {
85 self.regex.is_match(line)
86 }
87
88 pub fn extract_from_line(&self, line: &str) -> Result<Option<Option<Box<dyn Problem>>>, Error> {
99 let c = self.regex.captures(line);
100 if let Some(c) = c {
101 return Ok(Some((self.callback)(&c)?));
102 }
103 Ok(None)
104 }
105
106 fn origin(&self) -> Origin {
111 Origin(format!("direct regex ({})", self.regex.as_str()))
112 }
113}
114
115impl Matcher for RegexLineMatcher {
116 fn extract_from_lines(&self, lines: &[&str], offset: usize) -> MatchResult {
117 let line = lines[offset];
118 if let Some(problem) = self.extract_from_line(line)? {
119 let m = SingleLineMatch {
120 offset,
121 line: line.to_string(),
122 origin: self.origin(),
123 };
124 return Ok(Some((Box::new(m), problem)));
125 }
126 Ok(None)
127 }
128}
129
130#[macro_export]
151macro_rules! regex_line_matcher {
152 ($regex:expr, $callback:expr) => {
153 Box::new(RegexLineMatcher::new(
154 regex::Regex::new($regex).unwrap(),
155 Box::new($callback),
156 ))
157 };
158 ($regex: expr) => {
159 Box::new(RegexLineMatcher::new(
160 regex::Regex::new($regex).unwrap(),
161 Box::new(|_| Ok(None)),
162 ))
163 };
164}
165
166#[macro_export]
187macro_rules! regex_para_matcher {
188 ($regex:expr, $callback:expr) => {{
189 Box::new(RegexLineMatcher::new(
190 regex::Regex::new(concat!("(?s)", $regex)).unwrap(),
191 Box::new($callback),
192 ))
193 }};
194 ($regex: expr) => {{
195 Box::new(RegexLineMatcher::new(
196 regex::Regex::new(concat!("(?s)", $regex)).unwrap(),
197 Box::new(|_| Ok(None)),
198 ))
199 }};
200}
201
202pub struct MatcherGroup(Vec<Box<dyn Matcher>>);
207
208impl MatcherGroup {
209 pub fn new(matchers: Vec<Box<dyn Matcher>>) -> Self {
217 Self(matchers)
218 }
219}
220
221impl Default for MatcherGroup {
222 fn default() -> Self {
223 Self::new(vec![])
224 }
225}
226
227impl From<Vec<Box<dyn Matcher>>> for MatcherGroup {
228 fn from(matchers: Vec<Box<dyn Matcher>>) -> Self {
229 Self::new(matchers)
230 }
231}
232
233impl MatcherGroup {
234 pub fn extract_from_lines(&self, lines: &[&str], offset: usize) -> MatchResult {
248 for matcher in self.0.iter() {
249 if let Some(p) = matcher.extract_from_lines(lines, offset)? {
250 return Ok(Some(p));
251 }
252 }
253 Ok(None)
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use std::borrow::Cow;
261
262 #[derive(Debug)]
263 struct TestProblem {
264 description: String,
265 }
266
267 impl std::fmt::Display for TestProblem {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 write!(f, "{}", self.description)
270 }
271 }
272
273 impl Problem for TestProblem {
274 fn kind(&self) -> Cow<'_, str> {
275 Cow::Borrowed("test")
276 }
277
278 fn json(&self) -> serde_json::Value {
279 serde_json::json!({
280 "description": self.description,
281 })
282 }
283
284 fn as_any(&self) -> &dyn std::any::Any {
285 self
286 }
287 }
288
289 #[test]
290 fn test_error_display() {
291 let error = Error {
292 message: "test error".to_string(),
293 };
294 assert_eq!(error.to_string(), "test error");
295 }
296
297 #[test]
298 fn test_regex_line_matcher_new() {
299 let regex = Regex::new(r"test").unwrap();
300 let callback = Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> {
301 Ok(Some(Box::new(TestProblem {
302 description: "test problem".to_string(),
303 })))
304 });
305 let matcher = RegexLineMatcher::new(regex, callback);
306 assert!(matcher.matches_line("test line"));
307 assert!(!matcher.matches_line("other line"));
308 }
309
310 #[test]
311 fn test_regex_line_matcher_matches_line() {
312 let regex = Regex::new(r"test").unwrap();
313 let callback =
314 Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> { Ok(None) });
315 let matcher = RegexLineMatcher::new(regex, callback);
316 assert!(matcher.matches_line("test line"));
317 assert!(!matcher.matches_line("other line"));
318 }
319
320 #[test]
321 fn test_regex_line_matcher_extract_from_line() {
322 let regex = Regex::new(r"test").unwrap();
323 let callback = Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> {
324 Ok(Some(Box::new(TestProblem {
325 description: "test problem".to_string(),
326 })))
327 });
328 let matcher = RegexLineMatcher::new(regex, callback);
329 let result = matcher.extract_from_line("test line").unwrap();
330 assert!(result.is_some());
331 let problem = result.unwrap();
332 assert!(problem.is_some());
333 let problem = problem.unwrap();
334 assert_eq!(problem.kind(), "test");
335 }
336
337 #[test]
338 fn test_regex_line_matcher_extract_from_line_no_match() {
339 let regex = Regex::new(r"test").unwrap();
340 let callback = Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> {
341 Ok(Some(Box::new(TestProblem {
342 description: "test problem".to_string(),
343 })))
344 });
345 let matcher = RegexLineMatcher::new(regex, callback);
346 let result = matcher.extract_from_line("other line").unwrap();
347 assert!(result.is_none());
348 }
349
350 #[test]
351 fn test_regex_line_matcher_extract_from_line_no_problem() {
352 let regex = Regex::new(r"test").unwrap();
353 let callback =
354 Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> { Ok(None) });
355 let matcher = RegexLineMatcher::new(regex, callback);
356 let result = matcher.extract_from_line("test line").unwrap();
357 assert!(result.is_some());
358 let problem = result.unwrap();
359 assert!(problem.is_none());
360 }
361
362 #[test]
363 fn test_regex_line_matcher_extract_from_lines() {
364 let regex = Regex::new(r"test").unwrap();
365 let callback = Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> {
366 Ok(Some(Box::new(TestProblem {
367 description: "test problem".to_string(),
368 })))
369 });
370 let matcher = RegexLineMatcher::new(regex, callback);
371 let lines = vec!["line 1", "test line", "line 3"];
372 let result = matcher.extract_from_lines(&lines, 1).unwrap();
373 assert!(result.is_some());
374 let (m, problem) = result.unwrap();
375 assert_eq!(m.line(), "test line");
376 assert_eq!(m.offset(), 1);
377 assert!(problem.is_some());
378 let problem = problem.unwrap();
379 assert_eq!(problem.kind(), "test");
380 }
381
382 #[test]
383 fn test_regex_line_matcher_extract_from_lines_no_match() {
384 let regex = Regex::new(r"test").unwrap();
385 let callback = Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> {
386 Ok(Some(Box::new(TestProblem {
387 description: "test problem".to_string(),
388 })))
389 });
390 let matcher = RegexLineMatcher::new(regex, callback);
391 let lines = vec!["line 1", "line 2", "line 3"];
392 let result = matcher.extract_from_lines(&lines, 1).unwrap();
393 assert!(result.is_none());
394 }
395
396 #[test]
397 fn test_matcher_group() {
398 let regex1 = Regex::new(r"test1").unwrap();
399 let callback1 = Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> {
400 Ok(Some(Box::new(TestProblem {
401 description: "test problem 1".to_string(),
402 })))
403 });
404 let matcher1 = RegexLineMatcher::new(regex1, callback1);
405
406 let regex2 = Regex::new(r"test2").unwrap();
407 let callback2 = Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> {
408 Ok(Some(Box::new(TestProblem {
409 description: "test problem 2".to_string(),
410 })))
411 });
412 let matcher2 = RegexLineMatcher::new(regex2, callback2);
413
414 let group = MatcherGroup::new(vec![Box::new(matcher1), Box::new(matcher2)]);
415 let lines = vec!["line 1", "test2 line", "line 3"];
416 let result = group.extract_from_lines(&lines, 1).unwrap();
417 assert!(result.is_some());
418 let (m, problem) = result.unwrap();
419 assert_eq!(m.line(), "test2 line");
420 assert_eq!(m.offset(), 1);
421 assert!(problem.is_some());
422 let problem = problem.unwrap();
423 assert_eq!(problem.kind(), "test");
424 }
425
426 #[test]
427 fn test_matcher_group_no_match() {
428 let regex1 = Regex::new(r"test1").unwrap();
429 let callback1 = Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> {
430 Ok(Some(Box::new(TestProblem {
431 description: "test problem 1".to_string(),
432 })))
433 });
434 let matcher1 = RegexLineMatcher::new(regex1, callback1);
435
436 let regex2 = Regex::new(r"test2").unwrap();
437 let callback2 = Box::new(|_: &Captures| -> Result<Option<Box<dyn Problem>>, Error> {
438 Ok(Some(Box::new(TestProblem {
439 description: "test problem 2".to_string(),
440 })))
441 });
442 let matcher2 = RegexLineMatcher::new(regex2, callback2);
443
444 let group = MatcherGroup::new(vec![Box::new(matcher1), Box::new(matcher2)]);
445 let lines = vec!["line 1", "line 2", "line 3"];
446 let result = group.extract_from_lines(&lines, 1).unwrap();
447 assert!(result.is_none());
448 }
449
450 #[test]
451 fn test_regex_line_matcher_macro() {
452 let matcher = regex_line_matcher!(r"test", |_| {
453 Ok(Some(Box::new(TestProblem {
454 description: "test problem".to_string(),
455 })))
456 });
457 let lines = vec!["line 1", "test line", "line 3"];
458 let result = matcher.extract_from_lines(&lines, 1).unwrap();
459 assert!(result.is_some());
460 }
461
462 #[test]
463 fn test_regex_line_matcher_macro_simple() {
464 let matcher = regex_line_matcher!(r"test");
465 let lines = vec!["line 1", "test line", "line 3"];
466 let result = matcher.extract_from_lines(&lines, 1).unwrap();
467 assert!(result.is_some());
468 let (_m, problem) = result.unwrap();
469 assert!(problem.is_none());
470 }
471}