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 literal_patterns = config.patterns.clone();
29 } else {
30 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 pub fn has_match(&self, line: &str) -> bool {
113 self.match_line(line).matched
114 }
115
116 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
232 #[test]
233 fn test_has_match_coverage_line_112() {
234 let config = create_test_config("ERROR", false, false);
235 let matcher = Matcher::new(config);
236
237 assert!(matcher.has_match("ERROR: Something went wrong"));
239 assert!(!matcher.has_match("INFO: Normal operation"));
240 }
241
242 #[test]
243 fn test_get_all_matches_coverage_lines_122_129_139_141() {
244 let config = create_test_config("ERROR,WARN", false, false);
245 let matcher = Matcher::new(config);
246
247 let matches =
249 matcher.get_all_matches("ERROR: Something went wrong WARN: This is a warning");
250 assert_eq!(matches.len(), 2);
251 assert!(matches.contains(&"ERROR".to_string()));
252 assert!(matches.contains(&"WARN".to_string()));
253
254 let regex_config = create_test_config("ERROR,WARN", true, false);
256 let regex_matcher = Matcher::new(regex_config);
257 let regex_matches = regex_matcher.get_all_matches("ERROR: Something went wrong");
258 assert_eq!(regex_matches.len(), 1);
259 assert!(regex_matches.contains(&"ERROR".to_string()));
260 }
261
262 #[test]
263 fn test_case_insensitive_get_all_matches_coverage_line_122() {
264 let config = create_test_config("ERROR,WARN", false, true);
265 let matcher = Matcher::new(config);
266
267 let matches =
269 matcher.get_all_matches("error: Something went wrong warn: This is a warning");
270 assert_eq!(matches.len(), 2);
271 assert!(matches.contains(&"ERROR".to_string()));
272 assert!(matches.contains(&"WARN".to_string()));
273 }
274
275 #[test]
276 fn test_regex_get_all_matches_coverage_lines_139_141() {
277 let config = create_test_config("ERROR,WARN", true, false);
278 let matcher = Matcher::new(config);
279
280 let matches = matcher.get_all_matches("ERROR: Something went wrong");
282 assert_eq!(matches.len(), 1);
283 assert!(matches.contains(&"ERROR".to_string()));
284
285 let no_matches = matcher.get_all_matches("INFO: Normal operation");
287 assert!(no_matches.is_empty());
288 }
289}