1use std::path::PathBuf;
22
23use thiserror::Error;
24
25#[derive(Error, Debug)]
31pub enum Error {
32 #[error("I/O error: {0}")]
34 Io(#[from] std::io::Error),
35
36 #[error("I/O error at {path}: {message}")]
38 IoWithPath { path: PathBuf, message: String },
39
40 #[error("Configuration error: {0}")]
42 Config(String),
43
44 #[error("JSON error: {0}")]
46 Json(#[from] serde_json::Error),
47
48 #[error("YAML error: {0}")]
50 Yaml(#[from] serde_yaml::Error),
51
52 #[error("{resource_type} not found: {id}")]
54 NotFound { resource_type: String, id: String },
55
56 #[error("File not found: {}", path.display())]
58 FileNotFound { path: PathBuf },
59
60 #[error("Invalid path {}: {reason}", path.display())]
62 InvalidPath { path: PathBuf, reason: String },
63
64 #[error("Parse error: {0}")]
66 Parse(String),
67
68 #[error("{0}")]
70 Operation(String),
71}
72
73impl Error {
74 pub fn io(err: std::io::Error) -> Self {
83 Self::Io(err)
84 }
85
86 pub fn io_with_path(err: std::io::Error, path: impl Into<PathBuf>) -> Self {
88 Self::IoWithPath {
89 path: path.into(),
90 message: err.to_string(),
91 }
92 }
93
94 pub fn config(msg: impl Into<String>) -> Self {
96 Self::Config(msg.into())
97 }
98
99 pub fn not_found(resource_type: impl Into<String>, id: impl Into<String>) -> Self {
101 Self::NotFound {
102 resource_type: resource_type.into(),
103 id: id.into(),
104 }
105 }
106
107 pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
109 Self::FileNotFound { path: path.into() }
110 }
111
112 pub fn not_found_msg(msg: impl Into<String>) -> Self {
117 Self::NotFound {
118 resource_type: "Resource".to_string(),
119 id: msg.into(),
120 }
121 }
122
123 pub fn invalid_path(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
125 Self::InvalidPath {
126 path: path.into(),
127 reason: reason.into(),
128 }
129 }
130
131 pub fn parse(msg: impl Into<String>) -> Self {
133 Self::Parse(msg.into())
134 }
135
136 pub fn operation(msg: impl Into<String>) -> Self {
138 Self::Operation(msg.into())
139 }
140
141 pub fn is_io(&self) -> bool {
147 matches!(self, Self::Io(_) | Self::IoWithPath { .. })
148 }
149
150 pub fn is_not_found(&self) -> bool {
152 matches!(self, Self::NotFound { .. } | Self::FileNotFound { .. })
153 }
154
155 pub fn is_config(&self) -> bool {
157 matches!(self, Self::Config(_))
158 }
159
160 pub fn is_path_error(&self) -> bool {
162 matches!(
163 self,
164 Self::InvalidPath { .. } | Self::FileNotFound { .. } | Self::IoWithPath { .. }
165 )
166 }
167
168 pub fn is_parse(&self) -> bool {
170 matches!(self, Self::Parse(_))
171 }
172}
173
174pub type Result<T> = std::result::Result<T, Error>;
176
177pub fn log_error_chain(err: &dyn std::error::Error) {
193 let mut depth = 0;
194 let mut source = err.source();
195 while let Some(cause) = source {
196 depth += 1;
197 log::error!(" cause[{depth}]: {cause}");
198 log::debug!(" cause[{depth}] debug: {cause:?}");
199 source = cause.source();
200 }
201 if depth == 0 {
202 log::debug!(" (no further error sources in chain)");
203 }
204}
205
206#[cfg(test)]
211mod tests {
212 use super::*;
213 use std::error::Error as StdError;
214
215 #[test]
220 fn test_error_io_from() {
221 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
222 let err: Error = io_err.into();
223 assert!(err.is_io());
224 assert!(!err.is_not_found());
225 assert!(!err.is_config());
226 assert!(err.to_string().contains("I/O error"));
227 }
228
229 #[test]
230 fn test_error_io_constructor() {
231 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
232 let err = Error::io(io_err);
233 assert!(err.is_io());
234 assert!(err.to_string().contains("I/O error"));
235 assert!(err.to_string().contains("file not found"));
236 }
237
238 #[test]
239 fn test_error_io_with_path() {
240 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
241 let path = PathBuf::from("/test/path.txt");
242 let err = Error::io_with_path(io_err, &path);
243 assert!(err.is_io());
244 assert!(err.is_path_error());
245 let msg = err.to_string();
246 assert!(msg.contains("I/O error at"));
247 assert!(msg.contains("/test/path.txt"));
248 assert!(msg.contains("permission denied"));
249 }
250
251 #[test]
252 fn test_error_config() {
253 let err = Error::config("invalid configuration");
254 assert!(err.is_config());
255 assert!(!err.is_io());
256 assert!(!err.is_not_found());
257 assert!(err.to_string().contains("Configuration error"));
258 assert!(err.to_string().contains("invalid configuration"));
259 }
260
261 #[test]
262 fn test_error_not_found() {
263 let err = Error::not_found("Concept", "major-triad");
264 assert!(err.is_not_found());
265 assert!(!err.is_io());
266 assert!(!err.is_config());
267 let msg = err.to_string();
268 assert!(msg.contains("Concept not found"));
269 assert!(msg.contains("major-triad"));
270 }
271
272 #[test]
273 fn test_error_file_not_found() {
274 let path = PathBuf::from("/missing/file.txt");
275 let err = Error::file_not_found(&path);
276 assert!(err.is_not_found());
277 assert!(err.is_path_error());
278 assert!(!err.is_io());
279 let msg = err.to_string();
280 assert!(msg.contains("File not found"));
281 assert!(msg.contains("/missing/file.txt"));
282 }
283
284 #[test]
285 fn test_error_not_found_msg() {
286 let err = Error::not_found_msg("file with id 'xyz' not in cache");
287 assert!(err.is_not_found());
288 assert!(!err.is_io());
289 let msg = err.to_string();
290 assert!(msg.contains("Resource not found"));
291 assert!(msg.contains("file with id 'xyz' not in cache"));
292 }
293
294 #[test]
295 fn test_error_invalid_path() {
296 let path = PathBuf::from("/bad/path");
297 let err = Error::invalid_path(&path, "invalid characters");
298 assert!(err.is_path_error());
299 assert!(!err.is_io());
300 assert!(!err.is_not_found());
301 let msg = err.to_string();
302 assert!(msg.contains("Invalid path"));
303 assert!(msg.contains("/bad/path"));
304 assert!(msg.contains("invalid characters"));
305 }
306
307 #[test]
308 fn test_error_parse() {
309 let err = Error::parse("syntax error at line 5");
310 assert!(err.is_parse());
311 assert!(!err.is_io());
312 assert!(!err.is_config());
313 assert!(err.to_string().contains("Parse error"));
314 assert!(err.to_string().contains("syntax error at line 5"));
315 }
316
317 #[test]
318 fn test_error_operation() {
319 let err = Error::operation("index corrupted");
320 assert!(!err.is_io());
321 assert!(!err.is_not_found());
322 assert!(!err.is_config());
323 assert!(err.to_string().contains("index corrupted"));
324 }
325
326 #[test]
331 fn test_error_from_io_error() {
332 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "connection lost");
333 let err: Error = io_err.into();
334 assert!(err.is_io());
335 assert!(err.to_string().contains("connection lost"));
336 }
337
338 #[test]
339 fn test_error_from_json_error() {
340 let json_str = "{ invalid json }";
341 let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
342 let err: Error = json_err.into();
343 assert!(matches!(err, Error::Json(_)));
344 assert!(err.to_string().contains("JSON error"));
345 }
346
347 #[test]
352 fn test_error_source_io() {
353 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
354 let err: Error = io_err.into();
355 assert!(err.source().is_some());
356 }
357
358 #[test]
359 fn test_error_source_non_io() {
360 let err = Error::config("test");
361 assert!(err.source().is_none());
362 }
363
364 #[test]
369 fn test_error_display_all_variants() {
370 let errors = vec![
371 Error::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "io")),
372 Error::io_with_path(
373 std::io::Error::new(std::io::ErrorKind::NotFound, "io"),
374 "/path",
375 ),
376 Error::config("config"),
377 Error::not_found("Type", "id"),
378 Error::file_not_found("/path"),
379 Error::invalid_path("/path", "reason"),
380 Error::parse("parse"),
381 Error::operation("operation"),
382 ];
383
384 for err in errors {
385 let display = err.to_string();
386 assert!(
387 !display.is_empty(),
388 "Display should produce non-empty string for {:?}",
389 err
390 );
391 }
392 }
393
394 #[test]
399 fn test_result_alias() {
400 let ok: Result<i32> = Ok(42);
401 assert_eq!(ok.ok(), Some(42));
402
403 let err: Result<i32> = Err(Error::config("bad"));
404 assert!(err.is_err());
405 }
406
407 #[test]
412 fn test_log_error_chain_no_source() {
413 let err = Error::config("standalone error");
415 log_error_chain(&err);
416 }
417
418 #[test]
419 fn test_log_error_chain_with_source() {
420 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "inner cause");
422 let err: Error = io_err.into();
423 log_error_chain(&err);
424 }
425
426 #[test]
427 fn test_log_error_chain_nested() {
428 let inner =
430 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
431 let err: Error = inner.into();
432 log_error_chain(&err);
434 }
435}