1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
6pub enum SearchError {
7 #[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 #[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 #[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 #[error("IO error: {0}")]
24 Io(#[from] std::io::Error),
25
26 #[error("Failed to execute ripgrep command: {0}")]
28 RipgrepExecutionFailed(String),
29
30 #[error("ripgrep output is not valid UTF-8: {0}")]
32 InvalidUtf8(#[from] std::string::FromUtf8Error),
33
34 #[error("Failed to parse file path: {0}")]
36 InvalidPath(String),
37
38 #[error("{0}")]
40 Generic(String),
41}
42
43impl SearchError {
44 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 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 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 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
80pub 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}