frep_core/
validation.rs

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
31/// Collects errors into an array
32pub 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}