1use crossterm::style::Stylize;
2use fancy_regex::Regex as FancyRegex;
3use ignore::{overrides::Override, overrides::OverrideBuilder};
4use regex::Regex;
5use std::path::{Path, PathBuf};
6
7use crate::search::{FileSearcher, FileSearcherConfig, SearchType};
8use crate::utils;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
11#[allow(clippy::struct_excessive_bools)]
12pub struct SearchConfiguration<'a> {
13 pub search_text: &'a str,
14 pub replacement_text: &'a str,
15 pub fixed_strings: bool,
16 pub advanced_regex: bool,
17 pub include_globs: Option<&'a str>,
18 pub exclude_globs: Option<&'a str>,
19 pub match_whole_word: bool,
20 pub match_case: bool,
21 pub include_hidden: bool,
22 pub directory: PathBuf,
23}
24
25pub trait ValidationErrorHandler {
26 fn handle_search_text_error(&mut self, error: &str, detail: &str);
27 fn handle_include_files_error(&mut self, error: &str, detail: &str);
28 fn handle_exclude_files_error(&mut self, error: &str, detail: &str);
29}
30
31pub struct SimpleErrorHandler {
33 pub errors: Vec<String>,
34}
35
36impl SimpleErrorHandler {
37 pub fn new() -> Self {
38 Self { errors: Vec::new() }
39 }
40
41 pub fn errors_str(&self) -> Option<String> {
42 if self.errors.is_empty() {
43 None
44 } else {
45 Some(format!("Validation errors:\n{}", self.errors.join("\n")))
46 }
47 }
48
49 fn push_error(&mut self, err_msg: &str, detail: &str) {
50 self.errors
51 .push(format!("\n{title}:\n{detail}", title = err_msg.red()));
52 }
53}
54
55impl Default for SimpleErrorHandler {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl ValidationErrorHandler for SimpleErrorHandler {
62 fn handle_search_text_error(&mut self, _error: &str, detail: &str) {
63 self.push_error("Failed to parse search text", detail);
64 }
65
66 fn handle_include_files_error(&mut self, _error: &str, detail: &str) {
67 self.push_error("Failed to parse include globs", detail);
68 }
69
70 fn handle_exclude_files_error(&mut self, _error: &str, detail: &str) {
71 self.push_error("Failed to parse exclude globs", detail);
72 }
73}
74
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub enum ValidationResult<T> {
77 Success(T),
78 ValidationErrors,
79}
80
81pub fn validate_search_configuration<H: ValidationErrorHandler>(
82 config: SearchConfiguration<'_>,
83 error_handler: &mut H,
84) -> anyhow::Result<ValidationResult<FileSearcher>> {
85 let search_pattern = parse_search_text(
86 config.search_text,
87 config.fixed_strings,
88 config.advanced_regex,
89 error_handler,
90 )?;
91
92 let overrides = parse_overrides(
93 &config.directory,
94 config.include_globs,
95 config.exclude_globs,
96 error_handler,
97 )?;
98
99 if let (ValidationResult::Success(search_pattern), ValidationResult::Success(overrides)) =
100 (search_pattern, overrides)
101 {
102 let searcher = FileSearcher::new(FileSearcherConfig {
103 search: search_pattern,
104 replace: config.replacement_text.to_owned(),
105 whole_word: config.match_whole_word,
106 match_case: config.match_case,
107 overrides,
108 root_dir: config.directory,
109 include_hidden: config.include_hidden,
110 });
111 Ok(ValidationResult::Success(searcher))
112 } else {
113 Ok(ValidationResult::ValidationErrors)
114 }
115}
116
117fn parse_search_text_inner(
118 search_text: &str,
119 fixed_strings: bool,
120 advanced_regex: bool,
121) -> anyhow::Result<SearchType> {
122 let result = if fixed_strings {
123 SearchType::Fixed(search_text.to_string())
124 } else if advanced_regex {
125 SearchType::PatternAdvanced(FancyRegex::new(search_text)?)
126 } else {
127 SearchType::Pattern(Regex::new(search_text)?)
128 };
129 Ok(result)
130}
131
132fn parse_search_text<H: ValidationErrorHandler>(
133 search_text: &str,
134 fixed_strings: bool,
135 advanced_regex: bool,
136 error_handler: &mut H,
137) -> anyhow::Result<ValidationResult<SearchType>> {
138 match parse_search_text_inner(search_text, fixed_strings, advanced_regex) {
139 Ok(pattern) => Ok(ValidationResult::Success(pattern)),
140 Err(e) => {
141 if utils::is_regex_error(&e) {
142 error_handler.handle_search_text_error("Couldn't parse regex", &e.to_string());
143 Ok(ValidationResult::ValidationErrors)
144 } else {
145 Err(e)
146 }
147 }
148 }
149}
150
151fn parse_overrides<H: ValidationErrorHandler>(
152 dir: &Path,
153 include_globs: Option<&str>,
154 exclude_globs: Option<&str>,
155 error_handler: &mut H,
156) -> anyhow::Result<ValidationResult<Override>> {
157 let mut overrides = OverrideBuilder::new(dir);
158 let mut success = true;
159
160 if let Some(include_globs) = include_globs {
161 if let Err(e) = utils::add_overrides(&mut overrides, include_globs, "") {
162 error_handler.handle_include_files_error("Couldn't parse glob pattern", &e.to_string());
163 success = false;
164 }
165 }
166 if let Some(exclude_globs) = exclude_globs {
167 if let Err(e) = utils::add_overrides(&mut overrides, exclude_globs, "!") {
168 error_handler.handle_exclude_files_error("Couldn't parse glob pattern", &e.to_string());
169 success = false;
170 }
171 }
172 if !success {
173 return Ok(ValidationResult::ValidationErrors);
174 }
175
176 Ok(ValidationResult::Success(overrides.build()?))
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use tempfile::TempDir;
183
184 fn create_test_config<'a>() -> SearchConfiguration<'a> {
185 let temp_dir = TempDir::new().unwrap();
186 SearchConfiguration {
187 search_text: "test",
188 replacement_text: "replacement",
189 fixed_strings: false,
190 advanced_regex: false,
191 include_globs: Some("*.rs"),
192 exclude_globs: Some("target/*"),
193 match_whole_word: false,
194 match_case: false,
195 include_hidden: false,
196 directory: temp_dir.path().to_path_buf(),
197 }
198 }
199
200 #[test]
201 fn test_valid_configuration() {
202 let config = create_test_config();
203 let mut error_handler = SimpleErrorHandler::new();
204
205 let result = validate_search_configuration(config, &mut error_handler);
206
207 assert!(result.is_ok());
208 assert!(matches!(result.unwrap(), ValidationResult::Success(_)));
209 assert!(error_handler.errors_str().is_none());
210 }
211
212 #[test]
213 fn test_invalid_regex() {
214 let mut config = create_test_config();
215 config.search_text = "[invalid regex";
216 let mut error_handler = SimpleErrorHandler::new();
217
218 let result = validate_search_configuration(config, &mut error_handler);
219
220 assert!(result.is_ok());
221 assert!(matches!(
222 result.unwrap(),
223 ValidationResult::ValidationErrors
224 ));
225 assert!(error_handler.errors_str().is_some());
226 assert!(error_handler.errors[0].contains("Failed to parse search text"));
227 }
228
229 #[test]
230 fn test_invalid_include_glob() {
231 let mut config = create_test_config();
232 config.include_globs = Some("[invalid");
233 let mut error_handler = SimpleErrorHandler::new();
234
235 let result = validate_search_configuration(config, &mut error_handler);
236
237 assert!(result.is_ok());
238 assert!(matches!(
239 result.unwrap(),
240 ValidationResult::ValidationErrors
241 ));
242 assert!(error_handler.errors_str().is_some());
243 assert!(error_handler.errors[0].contains("Failed to parse include globs"));
244 }
245
246 #[test]
247 fn test_fixed_strings_mode() {
248 let mut config = create_test_config();
249 config.search_text = "[this would be invalid regex]";
250 config.fixed_strings = true;
251 let mut error_handler = SimpleErrorHandler::new();
252
253 let result = validate_search_configuration(config, &mut error_handler);
254
255 assert!(result.is_ok());
256 assert!(matches!(result.unwrap(), ValidationResult::Success(_)));
257 assert!(error_handler.errors_str().is_none());
258 }
259}