cs/error.rs
1//! # Error Handling - Rust Book Chapter 9
2//!
3//! This module demonstrates custom error types and error handling patterns from
4//! [The Rust Book Chapter 9](https://doc.rust-lang.org/book/ch09-00-error-handling.html).
5//!
6//! ## Key Concepts Demonstrated
7//!
8//! 1. **Custom Error Types with `thiserror`** (Chapter 9.2)
9//! - Using enums to represent different error conditions
10//! - Adding context to errors with struct variants
11//! - Automatic `Display` implementation via `#[error(...)]`
12//!
13//! 2. **Automatic Error Conversion with `#[from]`** (Chapter 9.2)
14//! - The `#[from]` attribute implements `From<OtherError>` automatically
15//! - Enables using `?` operator with different error types
16//!
17//! 3. **Type Alias for Results** (Chapter 9.2)
18//! - Using `type Result<T> = std::result::Result<T, SearchError>`
19//! - Makes function signatures cleaner
20//!
21//! 4. **Builder Methods with `impl Into<T>`** (Chapter 10.2)
22//! - Accepting flexible input types that can convert to the target type
23//! - Makes APIs more ergonomic
24//!
25//! ## Learning Notes
26//!
27//! The `thiserror` crate is industry standard for libraries because it:
28//! - Derives `std::error::Error` automatically
29//! - Generates helpful `Display` messages
30//! - Integrates seamlessly with the `?` operator
31//!
32//! Compare this to the book's manual error implementations to see how
33//! derive macros reduce boilerplate while maintaining full functionality.
34
35use std::path::PathBuf;
36use thiserror::Error;
37
38/// Custom error type for code search operations.
39///
40/// # Rust Book Reference
41///
42/// **Chapter 9.2: Recoverable Errors with Result**
43/// https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html
44///
45/// This demonstrates defining custom error types using an enum, which allows
46/// representing multiple kinds of errors with different associated data.
47///
48/// # Educational Notes
49///
50/// ## Why use an enum for errors?
51/// - Each variant can carry different data (strings, paths, numbers)
52/// - Pattern matching ensures all error cases are handled
53/// - Type system prevents mixing up different error kinds
54///
55/// ## The `#[derive(Debug, Error)]` attributes
56/// - `Debug`: Required by `std::error::Error` trait
57/// - `Error`: From `thiserror`, auto-implements `std::error::Error`
58///
59/// ## The `#[error("...")]` attribute
60/// - Automatically implements `Display` trait
61/// - Use `{field}` to interpolate struct fields
62/// - Creates user-friendly error messages
63///
64/// Compare this to the book's manual `Display` implementation in Chapter 9.2
65/// to see how `thiserror` reduces boilerplate.
66#[derive(Debug, Error)]
67pub enum SearchError {
68 /// No translation files found containing the search text
69 #[error("No translation files found containing '{text}'.\n\nSearched in: {searched_paths}\n\nTip: Check your project structure or verify translation files exist")]
70 NoTranslationFiles {
71 text: String,
72 searched_paths: String,
73 },
74
75 /// Failed to parse YAML file
76 #[error(
77 "Failed to parse YAML file {file}:\n{reason}\n\nTip: Verify the YAML syntax is correct"
78 )]
79 YamlParseError { file: PathBuf, reason: String },
80
81 /// Failed to parse JSON file
82 #[error(
83 "Failed to parse JSON file {file}:\n{reason}\n\nTip: Verify the JSON syntax is correct"
84 )]
85 JsonParseError { file: PathBuf, reason: String },
86
87 /// Translation key found but no code references detected
88 #[error("Translation key '{key}' found in {file} but no code references detected.\n\nTip: Check if the key is actually used in the codebase")]
89 NoCodeReferences { key: String, file: PathBuf },
90
91 /// IO error occurred during file operations.
92 ///
93 /// # Rust Book Reference
94 ///
95 /// **Chapter 9.2: Propagating Errors**
96 /// https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#propagating-errors
97 ///
98 /// # Educational Notes - The `#[from]` Attribute
99 ///
100 /// The `#[from]` attribute automatically implements:
101 /// ```rust,ignore
102 /// impl From<std::io::Error> for SearchError {
103 /// fn from(err: std::io::Error) -> Self {
104 /// SearchError::Io(err)
105 /// }
106 /// }
107 /// ```
108 ///
109 /// This enables the `?` operator to automatically convert IO errors:
110 /// ```rust,ignore
111 /// fn read_file(path: &Path) -> Result<String> {
112 /// let contents = std::fs::read_to_string(path)?; // IO error auto-converts
113 /// Ok(contents)
114 /// }
115 /// ```
116 ///
117 /// Without `#[from]`, you would need manual conversion or `.map_err()`.
118 ///
119 /// **Key Point**: The `?` operator calls `From::from` automatically,
120 /// making error propagation seamless across different error types.
121 #[error("IO error: {0}")]
122 Io(#[from] std::io::Error),
123
124 /// Failed to execute ripgrep command
125 #[error("Failed to execute ripgrep command: {0}")]
126 RipgrepExecutionFailed(String),
127
128 /// Invalid UTF-8 in ripgrep output
129 #[error("ripgrep output is not valid UTF-8: {0}")]
130 InvalidUtf8(#[from] std::string::FromUtf8Error),
131
132 /// Failed to parse file path
133 #[error("Failed to parse file path: {0}")]
134 InvalidPath(String),
135
136 /// Generic search error with context
137 #[error("{0}")]
138 Generic(String),
139}
140
141impl SearchError {
142 /// Create a NoTranslationFiles error with default searched paths.
143 ///
144 /// # Rust Book Reference
145 ///
146 /// **Chapter 10.2: Traits as Parameters**
147 /// https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
148 ///
149 /// # Educational Notes - `impl Into<String>`
150 ///
151 /// Using `impl Into<String>` instead of `String` makes the API more flexible:
152 ///
153 /// ```rust,ignore
154 /// // All of these work:
155 /// SearchError::no_translation_files("add new"); // &str
156 /// SearchError::no_translation_files(String::from("add new")); // String
157 /// SearchError::no_translation_files(owned_string); // String (moved)
158 /// ```
159 ///
160 /// **How it works:**
161 /// - `&str` implements `Into<String>` (converts by allocating)
162 /// - `String` implements `Into<String>` (converts by identity/move)
163 /// - The `.into()` call inside performs the conversion
164 ///
165 /// **Trade-off:**
166 /// - Pro: Caller convenience - accepts multiple types
167 /// - Pro: Follows Rust API guidelines
168 /// - Con: Slightly less clear what conversions happen
169 ///
170 /// **Best Practice**: Use `impl Into<T>` for owned types in constructors/builders,
171 /// use `&str` for borrowed parameters in regular methods.
172 pub fn no_translation_files(text: impl Into<String>) -> Self {
173 Self::NoTranslationFiles {
174 text: text.into(),
175 searched_paths: "config/locales, src/i18n, locales, i18n".to_string(),
176 }
177 }
178
179 /// Create a NoTranslationFiles error with custom searched paths.
180 ///
181 /// # Example
182 ///
183 /// ```rust,ignore
184 /// use code_search_cli::error::SearchError;
185 ///
186 /// let err = SearchError::no_translation_files_with_paths(
187 /// "Add New",
188 /// "src/locales, config/i18n"
189 /// );
190 /// ```
191 ///
192 /// Both parameters accept `&str` or `String` thanks to `impl Into<String>`.
193 pub fn no_translation_files_with_paths(
194 text: impl Into<String>,
195 paths: impl Into<String>,
196 ) -> Self {
197 Self::NoTranslationFiles {
198 text: text.into(),
199 searched_paths: paths.into(),
200 }
201 }
202
203 /// Create a YamlParseError from a file path and error.
204 ///
205 /// # Educational Notes - Multiple Generic Parameters
206 ///
207 /// This method shows using `impl Into<T>` with different types:
208 /// - `impl Into<PathBuf>` accepts `&Path`, `PathBuf`, `&str`, `String`
209 /// - `impl Into<String>` accepts `&str`, `String`
210 ///
211 /// Each parameter independently accepts its own set of convertible types.
212 pub fn yaml_parse_error(file: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
213 Self::YamlParseError {
214 file: file.into(),
215 reason: reason.into(),
216 }
217 }
218
219 /// Create a JsonParseError from a file path and error
220 pub fn json_parse_error(file: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
221 Self::JsonParseError {
222 file: file.into(),
223 reason: reason.into(),
224 }
225 }
226
227 /// Create a NoCodeReferences error
228 pub fn no_code_references(key: impl Into<String>, file: impl Into<PathBuf>) -> Self {
229 Self::NoCodeReferences {
230 key: key.into(),
231 file: file.into(),
232 }
233 }
234}
235
236/// Result type alias for SearchError
237pub type Result<T> = std::result::Result<T, SearchError>;
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_no_translation_files_error() {
245 let err = SearchError::no_translation_files("add new");
246 let msg = err.to_string();
247 assert!(msg.contains("add new"));
248 assert!(msg.contains("config/locales"));
249 assert!(msg.contains("Tip:"));
250 }
251
252 #[test]
253 fn test_no_translation_files_with_custom_paths() {
254 let err =
255 SearchError::no_translation_files_with_paths("test", "custom/path1, custom/path2");
256 let msg = err.to_string();
257 assert!(msg.contains("test"));
258 assert!(msg.contains("custom/path1"));
259 assert!(msg.contains("custom/path2"));
260 }
261
262 #[test]
263 fn test_yaml_parse_error() {
264 let err = SearchError::yaml_parse_error("config/en.yml", "unexpected character");
265 let msg = err.to_string();
266 assert!(msg.contains("config/en.yml"));
267 assert!(msg.contains("unexpected character"));
268 assert!(msg.contains("YAML syntax"));
269 }
270
271 #[test]
272 fn test_no_code_references_error() {
273 let err = SearchError::no_code_references("invoice.labels.add_new", "config/en.yml");
274 let msg = err.to_string();
275 assert!(msg.contains("invoice.labels.add_new"));
276 assert!(msg.contains("config/en.yml"));
277 assert!(msg.contains("Tip:"));
278 }
279
280 #[test]
281 fn test_io_error_conversion() {
282 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
283 let search_err: SearchError = io_err.into();
284 let msg = search_err.to_string();
285 assert!(msg.contains("IO error"));
286 assert!(msg.contains("file not found"));
287 }
288
289 #[test]
290 fn test_ripgrep_execution_failed() {
291 let err = SearchError::RipgrepExecutionFailed("command failed".to_string());
292 let msg = err.to_string();
293 assert!(msg.contains("Failed to execute ripgrep"));
294 assert!(msg.contains("command failed"));
295 }
296}