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