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(
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 #[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 #[error("IO error: {0}")]
26 Io(#[from] std::io::Error),
27
28 #[error("Failed to execute ripgrep command: {0}")]
30 RipgrepExecutionFailed(String),
31
32 #[error("ripgrep output is not valid UTF-8: {0}")]
34 InvalidUtf8(#[from] std::string::FromUtf8Error),
35
36 #[error("Failed to parse file path: {0}")]
38 InvalidPath(String),
39
40 #[error("{0}")]
42 Generic(String),
43}
44
45impl SearchError {
46 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 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 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 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
82pub 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}