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
177#[cfg(test)]
182mod tests {
183 use super::*;
184 use std::error::Error as StdError;
185
186 #[test]
191 fn test_error_io_from() {
192 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
193 let err: Error = io_err.into();
194 assert!(err.is_io());
195 assert!(!err.is_not_found());
196 assert!(!err.is_config());
197 assert!(err.to_string().contains("I/O error"));
198 }
199
200 #[test]
201 fn test_error_io_constructor() {
202 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
203 let err = Error::io(io_err);
204 assert!(err.is_io());
205 assert!(err.to_string().contains("I/O error"));
206 assert!(err.to_string().contains("file not found"));
207 }
208
209 #[test]
210 fn test_error_io_with_path() {
211 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
212 let path = PathBuf::from("/test/path.txt");
213 let err = Error::io_with_path(io_err, &path);
214 assert!(err.is_io());
215 assert!(err.is_path_error());
216 let msg = err.to_string();
217 assert!(msg.contains("I/O error at"));
218 assert!(msg.contains("/test/path.txt"));
219 assert!(msg.contains("permission denied"));
220 }
221
222 #[test]
223 fn test_error_config() {
224 let err = Error::config("invalid configuration");
225 assert!(err.is_config());
226 assert!(!err.is_io());
227 assert!(!err.is_not_found());
228 assert!(err.to_string().contains("Configuration error"));
229 assert!(err.to_string().contains("invalid configuration"));
230 }
231
232 #[test]
233 fn test_error_not_found() {
234 let err = Error::not_found("Concept", "major-triad");
235 assert!(err.is_not_found());
236 assert!(!err.is_io());
237 assert!(!err.is_config());
238 let msg = err.to_string();
239 assert!(msg.contains("Concept not found"));
240 assert!(msg.contains("major-triad"));
241 }
242
243 #[test]
244 fn test_error_file_not_found() {
245 let path = PathBuf::from("/missing/file.txt");
246 let err = Error::file_not_found(&path);
247 assert!(err.is_not_found());
248 assert!(err.is_path_error());
249 assert!(!err.is_io());
250 let msg = err.to_string();
251 assert!(msg.contains("File not found"));
252 assert!(msg.contains("/missing/file.txt"));
253 }
254
255 #[test]
256 fn test_error_not_found_msg() {
257 let err = Error::not_found_msg("file with id 'xyz' not in cache");
258 assert!(err.is_not_found());
259 assert!(!err.is_io());
260 let msg = err.to_string();
261 assert!(msg.contains("Resource not found"));
262 assert!(msg.contains("file with id 'xyz' not in cache"));
263 }
264
265 #[test]
266 fn test_error_invalid_path() {
267 let path = PathBuf::from("/bad/path");
268 let err = Error::invalid_path(&path, "invalid characters");
269 assert!(err.is_path_error());
270 assert!(!err.is_io());
271 assert!(!err.is_not_found());
272 let msg = err.to_string();
273 assert!(msg.contains("Invalid path"));
274 assert!(msg.contains("/bad/path"));
275 assert!(msg.contains("invalid characters"));
276 }
277
278 #[test]
279 fn test_error_parse() {
280 let err = Error::parse("syntax error at line 5");
281 assert!(err.is_parse());
282 assert!(!err.is_io());
283 assert!(!err.is_config());
284 assert!(err.to_string().contains("Parse error"));
285 assert!(err.to_string().contains("syntax error at line 5"));
286 }
287
288 #[test]
289 fn test_error_operation() {
290 let err = Error::operation("index corrupted");
291 assert!(!err.is_io());
292 assert!(!err.is_not_found());
293 assert!(!err.is_config());
294 assert!(err.to_string().contains("index corrupted"));
295 }
296
297 #[test]
302 fn test_error_from_io_error() {
303 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "connection lost");
304 let err: Error = io_err.into();
305 assert!(err.is_io());
306 assert!(err.to_string().contains("connection lost"));
307 }
308
309 #[test]
310 fn test_error_from_json_error() {
311 let json_str = "{ invalid json }";
312 let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
313 let err: Error = json_err.into();
314 assert!(matches!(err, Error::Json(_)));
315 assert!(err.to_string().contains("JSON error"));
316 }
317
318 #[test]
323 fn test_error_source_io() {
324 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
325 let err: Error = io_err.into();
326 assert!(err.source().is_some());
327 }
328
329 #[test]
330 fn test_error_source_non_io() {
331 let err = Error::config("test");
332 assert!(err.source().is_none());
333 }
334
335 #[test]
340 fn test_error_display_all_variants() {
341 let errors = vec![
342 Error::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "io")),
343 Error::io_with_path(
344 std::io::Error::new(std::io::ErrorKind::NotFound, "io"),
345 "/path",
346 ),
347 Error::config("config"),
348 Error::not_found("Type", "id"),
349 Error::file_not_found("/path"),
350 Error::invalid_path("/path", "reason"),
351 Error::parse("parse"),
352 Error::operation("operation"),
353 ];
354
355 for err in errors {
356 let display = err.to_string();
357 assert!(
358 !display.is_empty(),
359 "Display should produce non-empty string for {:?}",
360 err
361 );
362 }
363 }
364
365 #[test]
370 fn test_result_alias() {
371 let ok: Result<i32> = Ok(42);
372 assert_eq!(ok.ok(), Some(42));
373
374 let err: Result<i32> = Err(Error::config("bad"));
375 assert!(err.is_err());
376 }
377}