cs/
error.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4/// Custom error type for code search operations
5#[derive(Debug, Error)]
6pub enum SearchError {
7    /// No translation files found containing the search text
8    #[error("No translation files found containing '{text}'.\n\nSearched in: {searched_paths}\n\nTip: Check your project structure or verify translation files exist")]
9    NoTranslationFiles {
10        text: String,
11        searched_paths: String,
12    },
13
14    /// Failed to parse YAML file
15    #[error(
16        "Failed to parse YAML file {file}:\n{reason}\n\nTip: Verify the YAML syntax is correct"
17    )]
18    YamlParseError { file: PathBuf, reason: String },
19
20    /// Translation key found but no code references detected
21    #[error("Translation key '{key}' found in {file} but no code references detected.\n\nTip: Check if the key is actually used in the codebase")]
22    NoCodeReferences { key: String, file: PathBuf },
23
24    /// IO error occurred
25    #[error("IO error: {0}")]
26    Io(#[from] std::io::Error),
27
28    /// Failed to execute ripgrep command
29    #[error("Failed to execute ripgrep command: {0}")]
30    RipgrepExecutionFailed(String),
31
32    /// Invalid UTF-8 in ripgrep output
33    #[error("ripgrep output is not valid UTF-8: {0}")]
34    InvalidUtf8(#[from] std::string::FromUtf8Error),
35
36    /// Failed to parse file path
37    #[error("Failed to parse file path: {0}")]
38    InvalidPath(String),
39
40    /// Generic search error with context
41    #[error("{0}")]
42    Generic(String),
43}
44
45impl SearchError {
46    /// Create a NoTranslationFiles error with default searched paths
47    pub fn no_translation_files(text: impl Into<String>) -> Self {
48        Self::NoTranslationFiles {
49            text: text.into(),
50            searched_paths: "config/locales, src/i18n, locales, i18n".to_string(),
51        }
52    }
53
54    /// Create a NoTranslationFiles error with custom searched paths
55    pub fn no_translation_files_with_paths(
56        text: impl Into<String>,
57        paths: impl Into<String>,
58    ) -> Self {
59        Self::NoTranslationFiles {
60            text: text.into(),
61            searched_paths: paths.into(),
62        }
63    }
64
65    /// Create a YamlParseError from a file path and error
66    pub fn yaml_parse_error(file: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
67        Self::YamlParseError {
68            file: file.into(),
69            reason: reason.into(),
70        }
71    }
72
73    /// Create a NoCodeReferences error
74    pub fn no_code_references(key: impl Into<String>, file: impl Into<PathBuf>) -> Self {
75        Self::NoCodeReferences {
76            key: key.into(),
77            file: file.into(),
78        }
79    }
80}
81
82/// Result type alias for SearchError
83pub type Result<T> = std::result::Result<T, SearchError>;
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_no_translation_files_error() {
91        let err = SearchError::no_translation_files("add new");
92        let msg = err.to_string();
93        assert!(msg.contains("add new"));
94        assert!(msg.contains("config/locales"));
95        assert!(msg.contains("Tip:"));
96    }
97
98    #[test]
99    fn test_no_translation_files_with_custom_paths() {
100        let err =
101            SearchError::no_translation_files_with_paths("test", "custom/path1, custom/path2");
102        let msg = err.to_string();
103        assert!(msg.contains("test"));
104        assert!(msg.contains("custom/path1"));
105        assert!(msg.contains("custom/path2"));
106    }
107
108    #[test]
109    fn test_yaml_parse_error() {
110        let err = SearchError::yaml_parse_error("config/en.yml", "unexpected character");
111        let msg = err.to_string();
112        assert!(msg.contains("config/en.yml"));
113        assert!(msg.contains("unexpected character"));
114        assert!(msg.contains("YAML syntax"));
115    }
116
117    #[test]
118    fn test_no_code_references_error() {
119        let err = SearchError::no_code_references("invoice.labels.add_new", "config/en.yml");
120        let msg = err.to_string();
121        assert!(msg.contains("invoice.labels.add_new"));
122        assert!(msg.contains("config/en.yml"));
123        assert!(msg.contains("Tip:"));
124    }
125
126    #[test]
127    fn test_io_error_conversion() {
128        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
129        let search_err: SearchError = io_err.into();
130        let msg = search_err.to_string();
131        assert!(msg.contains("IO error"));
132        assert!(msg.contains("file not found"));
133    }
134
135    #[test]
136    fn test_ripgrep_execution_failed() {
137        let err = SearchError::RipgrepExecutionFailed("command failed".to_string());
138        let msg = err.to_string();
139        assert!(msg.contains("Failed to execute ripgrep"));
140        assert!(msg.contains("command failed"));
141    }
142}