1use thiserror::Error;
2
3#[derive(Error, Debug)]
5pub enum LetheError {
6 #[error("Database error: {message}")]
8 Database { message: String },
9
10 #[error("Embedding error: {message}")]
12 Embedding { message: String },
13
14 #[error("Configuration error: {message}")]
16 Config { message: String },
17
18 #[error("Validation error in {field}: {reason}")]
20 Validation { field: String, reason: String },
21
22 #[error("IO error: {0}")]
24 Io(#[from] std::io::Error),
25
26 #[error("JSON serialization error: {0}")]
28 JsonSerialization(#[from] serde_json::Error),
29
30 #[error("YAML serialization error: {0}")]
32 YamlSerialization(#[from] serde_yaml::Error),
33
34 #[cfg(feature = "ollama")]
36 #[error("HTTP client error: {0}")]
37 Http(#[from] reqwest::Error),
38
39 #[error("Operation timed out: {operation} after {timeout_ms}ms")]
41 Timeout { operation: String, timeout_ms: u64 },
42
43 #[error("Resource not found: {resource_type} with id {id}")]
45 NotFound { resource_type: String, id: String },
46
47 #[error("Authentication failed: {message}")]
49 Authentication { message: String },
50
51 #[error("Authorization failed: {message}")]
53 Authorization { message: String },
54
55 #[error("External service error: {service} - {message}")]
57 ExternalService { service: String, message: String },
58
59 #[error("Pipeline error: {stage} - {message}")]
61 Pipeline { stage: String, message: String },
62
63 #[error("Vector operation error: {message}")]
65 Vector { message: String },
66
67 #[error("Mathematical optimization error: {message}")]
69 MathOptimization { message: String },
70
71 #[error("Internal error: {message}")]
73 Internal { message: String },
74}
75
76pub type Result<T> = std::result::Result<T, LetheError>;
78
79impl LetheError {
80 pub fn database(message: impl Into<String>) -> Self {
82 Self::Database {
83 message: message.into(),
84 }
85 }
86
87 pub fn embedding(message: impl Into<String>) -> Self {
89 Self::Embedding {
90 message: message.into(),
91 }
92 }
93
94 pub fn config(message: impl Into<String>) -> Self {
96 Self::Config {
97 message: message.into(),
98 }
99 }
100
101 pub fn validation(field: impl Into<String>, reason: impl Into<String>) -> Self {
103 Self::Validation {
104 field: field.into(),
105 reason: reason.into(),
106 }
107 }
108
109 pub fn timeout(operation: impl Into<String>, timeout_ms: u64) -> Self {
111 Self::Timeout {
112 operation: operation.into(),
113 timeout_ms,
114 }
115 }
116
117 pub fn not_found(resource_type: impl Into<String>, id: impl Into<String>) -> Self {
119 Self::NotFound {
120 resource_type: resource_type.into(),
121 id: id.into(),
122 }
123 }
124
125 pub fn external_service(service: impl Into<String>, message: impl Into<String>) -> Self {
127 Self::ExternalService {
128 service: service.into(),
129 message: message.into(),
130 }
131 }
132
133 pub fn pipeline(stage: impl Into<String>, message: impl Into<String>) -> Self {
135 Self::Pipeline {
136 stage: stage.into(),
137 message: message.into(),
138 }
139 }
140
141 pub fn vector(message: impl Into<String>) -> Self {
143 Self::Vector {
144 message: message.into(),
145 }
146 }
147
148 pub fn internal(message: impl Into<String>) -> Self {
150 Self::Internal {
151 message: message.into(),
152 }
153 }
154}
155
156#[cfg(feature = "database")]
157impl From<sqlx::Error> for LetheError {
158 fn from(err: sqlx::Error) -> Self {
159 Self::database(err.to_string())
160 }
161}
162
163impl From<validator::ValidationErrors> for LetheError {
164 fn from(err: validator::ValidationErrors) -> Self {
165 Self::validation("validation", err.to_string())
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use std::io;
173
174 #[test]
176 fn test_database_error() {
177 let err = LetheError::database("Connection failed");
178 assert!(matches!(err, LetheError::Database { .. }));
179 assert_eq!(err.to_string(), "Database error: Connection failed");
180 }
181
182 #[test]
183 fn test_embedding_error() {
184 let err = LetheError::embedding("Model not available");
185 assert!(matches!(err, LetheError::Embedding { .. }));
186 assert_eq!(err.to_string(), "Embedding error: Model not available");
187 }
188
189 #[test]
190 fn test_config_error() {
191 let err = LetheError::config("Invalid configuration");
192 assert!(matches!(err, LetheError::Config { .. }));
193 assert_eq!(err.to_string(), "Configuration error: Invalid configuration");
194 }
195
196 #[test]
197 fn test_validation_error() {
198 let err = LetheError::validation("email", "Invalid format");
199 assert!(matches!(err, LetheError::Validation { .. }));
200 assert_eq!(err.to_string(), "Validation error in email: Invalid format");
201 }
202
203 #[test]
204 fn test_timeout_error() {
205 let err = LetheError::timeout("database_query", 5000);
206 assert!(matches!(err, LetheError::Timeout { .. }));
207 assert_eq!(err.to_string(), "Operation timed out: database_query after 5000ms");
208 }
209
210 #[test]
211 fn test_not_found_error() {
212 let err = LetheError::not_found("user", "123");
213 assert!(matches!(err, LetheError::NotFound { .. }));
214 assert_eq!(err.to_string(), "Resource not found: user with id 123");
215 }
216
217 #[test]
218 fn test_external_service_error() {
219 let err = LetheError::external_service("openai", "API rate limit exceeded");
220 assert!(matches!(err, LetheError::ExternalService { .. }));
221 assert_eq!(err.to_string(), "External service error: openai - API rate limit exceeded");
222 }
223
224 #[test]
225 fn test_pipeline_error() {
226 let err = LetheError::pipeline("embedding", "Failed to encode text");
227 assert!(matches!(err, LetheError::Pipeline { .. }));
228 assert_eq!(err.to_string(), "Pipeline error: embedding - Failed to encode text");
229 }
230
231 #[test]
232 fn test_vector_error() {
233 let err = LetheError::vector("Dimension mismatch");
234 assert!(matches!(err, LetheError::Vector { .. }));
235 assert_eq!(err.to_string(), "Vector operation error: Dimension mismatch");
236 }
237
238 #[test]
239 fn test_internal_error() {
240 let err = LetheError::internal("Unexpected state");
241 assert!(matches!(err, LetheError::Internal { .. }));
242 assert_eq!(err.to_string(), "Internal error: Unexpected state");
243 }
244
245 #[test]
247 fn test_authentication_error_variant() {
248 let err = LetheError::Authentication {
249 message: "Invalid token".to_string()
250 };
251 assert!(matches!(err, LetheError::Authentication { .. }));
252 assert_eq!(err.to_string(), "Authentication failed: Invalid token");
253 }
254
255 #[test]
256 fn test_authorization_error_variant() {
257 let err = LetheError::Authorization {
258 message: "Insufficient permissions".to_string()
259 };
260 assert!(matches!(err, LetheError::Authorization { .. }));
261 assert_eq!(err.to_string(), "Authorization failed: Insufficient permissions");
262 }
263
264 #[test]
265 fn test_math_optimization_error_variant() {
266 let err = LetheError::MathOptimization {
267 message: "Convergence failed".to_string()
268 };
269 assert!(matches!(err, LetheError::MathOptimization { .. }));
270 assert_eq!(err.to_string(), "Mathematical optimization error: Convergence failed");
271 }
272
273 #[test]
275 fn test_from_io_error() {
276 let io_err = io::Error::new(io::ErrorKind::NotFound, "File not found");
277 let lethe_err: LetheError = io_err.into();
278
279 assert!(matches!(lethe_err, LetheError::Io(_)));
280 assert!(lethe_err.to_string().contains("File not found"));
281 }
282
283 #[test]
284 fn test_from_serde_json_error() {
285 let json_err = serde_json::from_str::<serde_json::Value>("invalid json")
286 .unwrap_err();
287 let lethe_err: LetheError = json_err.into();
288
289 assert!(matches!(lethe_err, LetheError::JsonSerialization(_)));
290 assert!(lethe_err.to_string().contains("JSON serialization error"));
291 }
292
293 #[test]
294 fn test_from_reqwest_error() {
295 let req_err = reqwest::Client::new()
297 .get("not-a-valid-url")
298 .build()
299 .unwrap_err();
300 let lethe_err: LetheError = req_err.into();
301
302 assert!(matches!(lethe_err, LetheError::Http(_)));
303 assert!(lethe_err.to_string().contains("HTTP client error"));
304 }
305
306 #[test]
308 fn test_result_type_ok() {
309 let result: Result<i32> = Ok(42);
310 assert!(result.is_ok());
311 assert_eq!(result.unwrap(), 42);
312 }
313
314 #[test]
315 fn test_result_type_err() {
316 let result: Result<i32> = Err(LetheError::internal("Test error"));
317 assert!(result.is_err());
318 assert!(matches!(result.unwrap_err(), LetheError::Internal { .. }));
319 }
320
321 #[test]
323 fn test_error_chain_io() {
324 fn inner_function() -> std::io::Result<String> {
325 Err(io::Error::new(io::ErrorKind::PermissionDenied, "Access denied"))
326 }
327
328 fn outer_function() -> Result<String> {
329 let content = inner_function()?; Ok(content)
331 }
332
333 let result = outer_function();
334 assert!(result.is_err());
335
336 let err = result.unwrap_err();
337 assert!(matches!(err, LetheError::Io(_)));
338 assert!(err.to_string().contains("Access denied"));
339 }
340
341 #[test]
342 fn test_error_chain_serialization() {
343 fn deserialize_config() -> Result<serde_json::Value> {
344 let config: serde_json::Value = serde_json::from_str("{invalid}")?;
345 Ok(config)
346 }
347
348 let result = deserialize_config();
349 assert!(result.is_err());
350
351 let err = result.unwrap_err();
352 assert!(matches!(err, LetheError::JsonSerialization(_)));
353 }
354
355 #[test]
357 fn test_error_debug_format() {
358 let err = LetheError::validation("field", "reason");
359 let debug_str = format!("{:?}", err);
360 assert!(debug_str.contains("Validation"));
361 assert!(debug_str.contains("field"));
362 assert!(debug_str.contains("reason"));
363 }
364
365 #[test]
366 fn test_error_display_format() {
367 let err = LetheError::database("Connection timeout");
368 let display_str = format!("{}", err);
369 assert_eq!(display_str, "Database error: Connection timeout");
370 }
371
372 #[test]
374 fn test_error_variants_are_different() {
375 let err1 = LetheError::database("message");
376 let err2 = LetheError::embedding("message");
377
378 assert_ne!(format!("{:?}", err1), format!("{:?}", err2));
380 }
381
382 #[test]
384 fn test_complex_error_scenario() {
385 fn process_user_data(data: &str) -> Result<String> {
386 if data.is_empty() {
388 return Err(LetheError::validation("data", "Cannot be empty"));
389 }
390
391 let parsed: serde_json::Value = serde_json::from_str(data)?;
393
394 if !parsed.is_object() {
396 return Err(LetheError::pipeline("validation", "Data must be an object"));
397 }
398
399 Ok("processed".to_string())
400 }
401
402 let result1 = process_user_data("");
404 assert!(result1.is_err());
405 assert!(matches!(result1.unwrap_err(), LetheError::Validation { .. }));
406
407 let result2 = process_user_data("invalid json");
409 assert!(result2.is_err());
410 assert!(matches!(result2.unwrap_err(), LetheError::JsonSerialization(_)));
411
412 let result3 = process_user_data("\"string\"");
414 assert!(result3.is_err());
415 assert!(matches!(result3.unwrap_err(), LetheError::Pipeline { .. }));
416
417 let result4 = process_user_data("{\"key\": \"value\"}");
419 assert!(result4.is_ok());
420 assert_eq!(result4.unwrap(), "processed");
421 }
422
423 #[test]
425 fn test_from_validator_errors() {
426 use validator::{Validate, ValidationErrors, ValidationError};
427
428 #[derive(Validate)]
429 struct TestStruct {
430 #[validate(length(min = 1))]
431 name: String,
432 }
433
434 let test_struct = TestStruct {
435 name: "".to_string(), };
437
438 let validation_result = test_struct.validate();
439 assert!(validation_result.is_err());
440
441 let validation_errors = validation_result.unwrap_err();
442 let lethe_error: LetheError = validation_errors.into();
443
444 assert!(matches!(lethe_error, LetheError::Validation { .. }));
445 assert!(lethe_error.to_string().contains("Validation error"));
446 }
447
448 #[cfg(feature = "database")]
450 #[test]
451 fn test_from_sqlx_error() {
452 let db_error = sqlx::Error::Configuration("Invalid database URL".into());
454 let lethe_error: LetheError = db_error.into();
455
456 assert!(matches!(lethe_error, LetheError::Database { .. }));
457 assert!(lethe_error.to_string().contains("Database error"));
458 assert!(lethe_error.to_string().contains("Invalid database URL"));
459 }
460
461 #[test]
463 fn test_error_source_chain() {
464 let io_err = io::Error::new(io::ErrorKind::NotFound, "Original cause");
465 let lethe_err: LetheError = io_err.into();
466
467 let source = std::error::Error::source(&lethe_err);
469 assert!(source.is_some());
470 assert_eq!(source.unwrap().to_string(), "Original cause");
471 }
472
473 #[test]
475 fn test_all_error_variants() {
476 let errors = vec![
477 LetheError::Database { message: "db".to_string() },
478 LetheError::Embedding { message: "embed".to_string() },
479 LetheError::Config { message: "config".to_string() },
480 LetheError::Validation { field: "field".to_string(), reason: "reason".to_string() },
481 LetheError::Io(io::Error::new(io::ErrorKind::Other, "io")),
482 LetheError::JsonSerialization(serde_json::from_str::<()>("invalid").unwrap_err()),
483 LetheError::Http(reqwest::Client::new().get("invalid-url").build().unwrap_err()),
484 LetheError::Timeout { operation: "op".to_string(), timeout_ms: 1000 },
485 LetheError::NotFound { resource_type: "user".to_string(), id: "123".to_string() },
486 LetheError::Authentication { message: "auth".to_string() },
487 LetheError::Authorization { message: "authz".to_string() },
488 LetheError::ExternalService { service: "svc".to_string(), message: "msg".to_string() },
489 LetheError::Pipeline { stage: "stage".to_string(), message: "msg".to_string() },
490 LetheError::Vector { message: "vector".to_string() },
491 LetheError::MathOptimization { message: "math".to_string() },
492 LetheError::Internal { message: "internal".to_string() },
493 ];
494
495 for err in errors {
497 let _ = format!("{}", err);
498 let _ = format!("{:?}", err);
499 }
500 }
501
502 #[test]
504 fn test_helper_functions_with_different_inputs() {
505 let err1 = LetheError::database("literal");
507 assert_eq!(err1.to_string(), "Database error: literal");
508
509 let msg = "owned string".to_string();
511 let err2 = LetheError::embedding(&msg);
512 assert_eq!(err2.to_string(), "Embedding error: owned string");
513
514 let slice = "slice";
516 let err3 = LetheError::config(slice);
517 assert_eq!(err3.to_string(), "Configuration error: slice");
518
519 let err4 = LetheError::validation("field", format!("error {}", 42));
521 assert_eq!(err4.to_string(), "Validation error in field: error 42");
522 }
523
524 #[test]
526 fn test_error_size_reasonable() {
527 use std::mem;
528
529 let size = mem::size_of::<LetheError>();
531
532 assert!(size < 500, "LetheError size is {} bytes, might be too large", size);
535 }
536
537 #[test]
539 fn test_question_mark_operator() {
540 fn function_that_fails() -> Result<i32> {
541 let _value: serde_json::Value = serde_json::from_str("invalid")?;
542 Ok(42)
543 }
544
545 let result = function_that_fails();
546 assert!(result.is_err());
547 assert!(matches!(result.unwrap_err(), LetheError::JsonSerialization(_)));
548 }
549
550 #[test]
552 fn test_error_downcast_patterns() {
553 let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied");
554 let lethe_err: LetheError = io_err.into();
555
556 match &lethe_err {
557 LetheError::Io(inner_err) => {
558 assert_eq!(inner_err.kind(), io::ErrorKind::PermissionDenied);
559 assert_eq!(inner_err.to_string(), "Permission denied");
560 },
561 _ => panic!("Expected Io variant"),
562 }
563 }
564
565 #[test]
567 fn test_multi_layer_error_propagation() {
568 fn data_layer() -> std::io::Result<String> {
569 Err(io::Error::new(io::ErrorKind::NotFound, "Data file not found"))
570 }
571
572 fn business_layer() -> Result<String> {
573 let data = data_layer()?; Ok(data.to_uppercase())
575 }
576
577 fn api_layer() -> Result<String> {
578 let processed = business_layer()?; Ok(format!("Response: {}", processed))
580 }
581
582 let result = api_layer();
583 assert!(result.is_err());
584
585 let error = result.unwrap_err();
586 assert!(matches!(error, LetheError::Io(_)));
587 assert!(error.to_string().contains("Data file not found"));
588 }
589
590 #[test]
592 fn test_custom_error_formatting() {
593 let err = LetheError::ExternalService {
594 service: "OpenAI".to_string(),
595 message: "Rate limit exceeded: 1000 requests/min".to_string(),
596 };
597
598 let formatted = err.to_string();
599 assert!(formatted.contains("External service error"));
600 assert!(formatted.contains("OpenAI"));
601 assert!(formatted.contains("Rate limit exceeded"));
602 }
603}