log_watcher/
matcher.rs

1use crate::config::Config;
2use regex::Regex;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
6pub struct MatchResult {
7    pub matched: bool,
8    pub pattern: Option<String>,
9    pub color: Option<termcolor::Color>,
10    pub should_notify: bool,
11}
12
13#[derive(Debug)]
14pub struct Matcher {
15    config: Config,
16    literal_patterns: Vec<String>,
17    regex_patterns: Vec<Regex>,
18    pattern_colors: HashMap<String, termcolor::Color>,
19}
20
21impl Matcher {
22    pub fn new(config: Config) -> Self {
23        let mut literal_patterns = Vec::new();
24        let mut regex_patterns = Vec::new();
25
26        if config.regex_patterns.is_empty() {
27            // Use literal patterns
28            literal_patterns = config.patterns.clone();
29        } else {
30            // Use regex patterns
31            regex_patterns = config.regex_patterns.clone();
32        }
33
34        let pattern_colors = config.color_mappings.clone();
35
36        Self {
37            config,
38            literal_patterns,
39            regex_patterns,
40            pattern_colors,
41        }
42    }
43
44    pub fn match_line(&self, line: &str) -> MatchResult {
45        if self.config.regex_patterns.is_empty() {
46            self.match_literal(line)
47        } else {
48            self.match_regex(line)
49        }
50    }
51
52    fn match_literal(&self, line: &str) -> MatchResult {
53        let search_line = if self.config.case_insensitive {
54            line.to_lowercase()
55        } else {
56            line.to_string()
57        };
58
59        for pattern in &self.literal_patterns {
60            let search_pattern = if self.config.case_insensitive {
61                pattern.to_lowercase()
62            } else {
63                pattern.clone()
64            };
65
66            if search_line.contains(&search_pattern) {
67                let color = self.pattern_colors.get(pattern).copied();
68                let should_notify = self.config.should_notify_for_pattern(pattern);
69
70                return MatchResult {
71                    matched: true,
72                    pattern: Some(pattern.clone()),
73                    color,
74                    should_notify,
75                };
76            }
77        }
78
79        MatchResult {
80            matched: false,
81            pattern: None,
82            color: None,
83            should_notify: false,
84        }
85    }
86
87    fn match_regex(&self, line: &str) -> MatchResult {
88        for (i, regex) in self.regex_patterns.iter().enumerate() {
89            if regex.is_match(line) {
90                let pattern = self.config.patterns.get(i).cloned().unwrap_or_default();
91                let color = self.pattern_colors.get(&pattern).copied();
92                let should_notify = self.config.should_notify_for_pattern(&pattern);
93
94                return MatchResult {
95                    matched: true,
96                    pattern: Some(pattern),
97                    color,
98                    should_notify,
99                };
100            }
101        }
102
103        MatchResult {
104            matched: false,
105            pattern: None,
106            color: None,
107            should_notify: false,
108        }
109    }
110
111    /// Check if any pattern matches (for quiet mode filtering)
112    pub fn has_match(&self, line: &str) -> bool {
113        self.match_line(line).matched
114    }
115
116    /// Get all patterns that match a line
117    pub fn get_all_matches(&self, line: &str) -> Vec<String> {
118        let mut matches = Vec::new();
119
120        if self.config.regex_patterns.is_empty() {
121            let search_line = if self.config.case_insensitive {
122                line.to_lowercase()
123            } else {
124                line.to_string()
125            };
126
127            for pattern in &self.literal_patterns {
128                let search_pattern = if self.config.case_insensitive {
129                    pattern.to_lowercase()
130                } else {
131                    pattern.clone()
132                };
133
134                if search_line.contains(&search_pattern) {
135                    matches.push(pattern.clone());
136                }
137            }
138        } else {
139            for (i, regex) in self.regex_patterns.iter().enumerate() {
140                if regex.is_match(line) {
141                    if let Some(pattern) = self.config.patterns.get(i) {
142                        matches.push(pattern.clone());
143                    }
144                }
145            }
146        }
147
148        matches
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::cli::Args;
156    use std::path::PathBuf;
157
158    fn create_test_config(patterns: &str, regex: bool, case_insensitive: bool) -> Config {
159        let args = Args {
160            files: vec![PathBuf::from("test.log")],
161            patterns: patterns.to_string(),
162            regex,
163            case_insensitive,
164            color_map: None,
165            notify: true,
166            notify_patterns: None,
167            notify_throttle: 5,
168            dry_run: false,
169            quiet: false,
170            no_color: false,
171            prefix_file: None,
172            poll_interval: 100,
173            buffer_size: 8192,
174        };
175        Config::from_args(&args).unwrap()
176    }
177
178    #[test]
179    fn test_literal_matching() {
180        let config = create_test_config("ERROR,WARN", false, false);
181        let matcher = Matcher::new(config);
182
183        let result = matcher.match_line("This is an ERROR message");
184        assert!(result.matched);
185        assert_eq!(result.pattern, Some("ERROR".to_string()));
186
187        let result = matcher.match_line("This is a WARN message");
188        assert!(result.matched);
189        assert_eq!(result.pattern, Some("WARN".to_string()));
190
191        let result = matcher.match_line("This is a normal message");
192        assert!(!result.matched);
193    }
194
195    #[test]
196    fn test_case_insensitive_matching() {
197        let config = create_test_config("ERROR", false, true);
198        let matcher = Matcher::new(config);
199
200        let result = matcher.match_line("This is an error message");
201        assert!(result.matched);
202        assert_eq!(result.pattern, Some("ERROR".to_string()));
203
204        let result = matcher.match_line("This is an ERROR message");
205        assert!(result.matched);
206        assert_eq!(result.pattern, Some("ERROR".to_string()));
207    }
208
209    #[test]
210    fn test_regex_matching() {
211        let config = create_test_config(r"user_id=\d+", true, false);
212        let matcher = Matcher::new(config);
213
214        let result = matcher.match_line("Login successful for user_id=12345");
215        assert!(result.matched);
216
217        let result = matcher.match_line("Login successful for user_id=abc");
218        assert!(!result.matched);
219    }
220
221    #[test]
222    fn test_multiple_matches() {
223        let config = create_test_config("ERROR,WARN", false, false);
224        let matcher = Matcher::new(config);
225
226        let matches = matcher.get_all_matches("ERROR: This is a WARN message");
227        assert_eq!(matches.len(), 2);
228        assert!(matches.contains(&"ERROR".to_string()));
229        assert!(matches.contains(&"WARN".to_string()));
230    }
231}