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