ragfs_core/
error.rs

1//! Error types for RAGFS.
2
3use thiserror::Error;
4
5/// Main error type for RAGFS operations.
6#[derive(Error, Debug)]
7pub enum Error {
8    /// Content extraction failed
9    #[error("extraction error: {0}")]
10    Extraction(#[from] ExtractError),
11
12    /// Chunking failed
13    #[error("chunking error: {0}")]
14    Chunking(#[from] ChunkError),
15
16    /// Embedding generation failed
17    #[error("embedding error: {0}")]
18    Embedding(#[from] EmbedError),
19
20    /// Vector store operation failed
21    #[error("store error: {0}")]
22    Store(#[from] StoreError),
23
24    /// I/O error
25    #[error("io error: {0}")]
26    Io(#[from] std::io::Error),
27
28    /// Serialization error
29    #[error("serialization error: {0}")]
30    Serialization(#[from] serde_json::Error),
31
32    /// Configuration error
33    #[error("config error: {0}")]
34    Config(String),
35
36    /// Generic error
37    #[error("{0}")]
38    Other(String),
39}
40
41/// Content extraction errors.
42#[derive(Error, Debug)]
43pub enum ExtractError {
44    #[error("unsupported file type: {0}")]
45    UnsupportedType(String),
46
47    #[error("parse error: {0}")]
48    Parse(String),
49
50    #[error("io error: {0}")]
51    Io(#[from] std::io::Error),
52
53    #[error("extraction failed: {0}")]
54    Failed(String),
55}
56
57/// Chunking errors.
58#[derive(Error, Debug)]
59pub enum ChunkError {
60    #[error("chunking failed: {0}")]
61    Failed(String),
62
63    #[error("invalid configuration: {0}")]
64    InvalidConfig(String),
65}
66
67/// Embedding errors.
68#[derive(Error, Debug)]
69pub enum EmbedError {
70    #[error("model loading failed: {0}")]
71    ModelLoad(String),
72
73    #[error("inference failed: {0}")]
74    Inference(String),
75
76    #[error("modality not supported: {0:?}")]
77    ModalityNotSupported(crate::types::Modality),
78
79    #[error("input too long: {tokens} tokens, max {max}")]
80    InputTooLong { tokens: usize, max: usize },
81}
82
83/// Vector store errors.
84#[derive(Error, Debug)]
85pub enum StoreError {
86    #[error("store initialization failed: {0}")]
87    Init(String),
88
89    #[error("insert failed: {0}")]
90    Insert(String),
91
92    #[error("query failed: {0}")]
93    Query(String),
94
95    #[error("delete failed: {0}")]
96    Delete(String),
97
98    #[error("schema error: {0}")]
99    Schema(String),
100}
101
102/// Result type alias for RAGFS operations.
103pub type Result<T> = std::result::Result<T, Error>;
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::types::Modality;
109
110    // ========== ExtractError Tests ==========
111
112    #[test]
113    fn test_extract_error_unsupported_type_display() {
114        let err = ExtractError::UnsupportedType("application/octet-stream".to_string());
115        assert_eq!(
116            err.to_string(),
117            "unsupported file type: application/octet-stream"
118        );
119    }
120
121    #[test]
122    fn test_extract_error_parse_display() {
123        let err = ExtractError::Parse("invalid UTF-8".to_string());
124        assert_eq!(err.to_string(), "parse error: invalid UTF-8");
125    }
126
127    #[test]
128    fn test_extract_error_io_display() {
129        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
130        let err = ExtractError::Io(io_err);
131        assert!(err.to_string().contains("file not found"));
132    }
133
134    #[test]
135    fn test_extract_error_failed_display() {
136        let err = ExtractError::Failed("PDF parsing crashed".to_string());
137        assert_eq!(err.to_string(), "extraction failed: PDF parsing crashed");
138    }
139
140    #[test]
141    fn test_extract_error_from_io() {
142        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
143        let err: ExtractError = io_err.into();
144        assert!(matches!(err, ExtractError::Io(_)));
145    }
146
147    // ========== ChunkError Tests ==========
148
149    #[test]
150    fn test_chunk_error_failed_display() {
151        let err = ChunkError::Failed("empty content".to_string());
152        assert_eq!(err.to_string(), "chunking failed: empty content");
153    }
154
155    #[test]
156    fn test_chunk_error_invalid_config_display() {
157        let err = ChunkError::InvalidConfig("target_size must be > 0".to_string());
158        assert_eq!(
159            err.to_string(),
160            "invalid configuration: target_size must be > 0"
161        );
162    }
163
164    // ========== EmbedError Tests ==========
165
166    #[test]
167    fn test_embed_error_model_load_display() {
168        let err = EmbedError::ModelLoad("weights file not found".to_string());
169        assert_eq!(
170            err.to_string(),
171            "model loading failed: weights file not found"
172        );
173    }
174
175    #[test]
176    fn test_embed_error_inference_display() {
177        let err = EmbedError::Inference("CUDA out of memory".to_string());
178        assert_eq!(err.to_string(), "inference failed: CUDA out of memory");
179    }
180
181    #[test]
182    fn test_embed_error_modality_not_supported_display() {
183        let err = EmbedError::ModalityNotSupported(Modality::Audio);
184        assert_eq!(err.to_string(), "modality not supported: Audio");
185    }
186
187    #[test]
188    fn test_embed_error_input_too_long_display() {
189        let err = EmbedError::InputTooLong {
190            tokens: 10000,
191            max: 8192,
192        };
193        assert_eq!(err.to_string(), "input too long: 10000 tokens, max 8192");
194    }
195
196    // ========== StoreError Tests ==========
197
198    #[test]
199    fn test_store_error_init_display() {
200        let err = StoreError::Init("database locked".to_string());
201        assert_eq!(
202            err.to_string(),
203            "store initialization failed: database locked"
204        );
205    }
206
207    #[test]
208    fn test_store_error_insert_display() {
209        let err = StoreError::Insert("duplicate key".to_string());
210        assert_eq!(err.to_string(), "insert failed: duplicate key");
211    }
212
213    #[test]
214    fn test_store_error_query_display() {
215        let err = StoreError::Query("invalid vector dimension".to_string());
216        assert_eq!(err.to_string(), "query failed: invalid vector dimension");
217    }
218
219    #[test]
220    fn test_store_error_delete_display() {
221        let err = StoreError::Delete("file not indexed".to_string());
222        assert_eq!(err.to_string(), "delete failed: file not indexed");
223    }
224
225    #[test]
226    fn test_store_error_schema_display() {
227        let err = StoreError::Schema("missing embedding column".to_string());
228        assert_eq!(err.to_string(), "schema error: missing embedding column");
229    }
230
231    // ========== Main Error Tests ==========
232
233    #[test]
234    fn test_error_from_extract_error() {
235        let extract_err = ExtractError::UnsupportedType("video/mp4".to_string());
236        let err: Error = extract_err.into();
237        assert!(matches!(err, Error::Extraction(_)));
238        assert!(err.to_string().contains("video/mp4"));
239    }
240
241    #[test]
242    fn test_error_from_chunk_error() {
243        let chunk_err = ChunkError::Failed("too short".to_string());
244        let err: Error = chunk_err.into();
245        assert!(matches!(err, Error::Chunking(_)));
246        assert!(err.to_string().contains("too short"));
247    }
248
249    #[test]
250    fn test_error_from_embed_error() {
251        let embed_err = EmbedError::ModelLoad("missing model".to_string());
252        let err: Error = embed_err.into();
253        assert!(matches!(err, Error::Embedding(_)));
254        assert!(err.to_string().contains("missing model"));
255    }
256
257    #[test]
258    fn test_error_from_store_error() {
259        let store_err = StoreError::Query("timeout".to_string());
260        let err: Error = store_err.into();
261        assert!(matches!(err, Error::Store(_)));
262        assert!(err.to_string().contains("timeout"));
263    }
264
265    #[test]
266    fn test_error_from_io_error() {
267        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
268        let err: Error = io_err.into();
269        assert!(matches!(err, Error::Io(_)));
270    }
271
272    #[test]
273    fn test_error_config_display() {
274        let err = Error::Config("invalid path".to_string());
275        assert_eq!(err.to_string(), "config error: invalid path");
276    }
277
278    #[test]
279    fn test_error_other_display() {
280        let err = Error::Other("unexpected condition".to_string());
281        assert_eq!(err.to_string(), "unexpected condition");
282    }
283
284    // ========== Error Debug Tests ==========
285
286    #[test]
287    fn test_extract_error_debug() {
288        let err = ExtractError::Parse("bad format".to_string());
289        let debug_str = format!("{err:?}");
290        assert!(debug_str.contains("Parse"));
291        assert!(debug_str.contains("bad format"));
292    }
293
294    #[test]
295    fn test_chunk_error_debug() {
296        let err = ChunkError::InvalidConfig("negative size".to_string());
297        let debug_str = format!("{err:?}");
298        assert!(debug_str.contains("InvalidConfig"));
299    }
300
301    #[test]
302    fn test_embed_error_debug() {
303        let err = EmbedError::InputTooLong {
304            tokens: 5000,
305            max: 4096,
306        };
307        let debug_str = format!("{err:?}");
308        assert!(debug_str.contains("InputTooLong"));
309        assert!(debug_str.contains("5000"));
310    }
311
312    #[test]
313    fn test_store_error_debug() {
314        let err = StoreError::Schema("wrong type".to_string());
315        let debug_str = format!("{err:?}");
316        assert!(debug_str.contains("Schema"));
317    }
318
319    #[test]
320    fn test_main_error_debug() {
321        let err = Error::Config("missing key".to_string());
322        let debug_str = format!("{err:?}");
323        assert!(debug_str.contains("Config"));
324    }
325
326    // ========== Error Chaining Tests ==========
327
328    #[test]
329    fn test_error_chain_io_to_extract_to_main() {
330        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file.txt not found");
331        let extract_err: ExtractError = io_err.into();
332        let main_err: Error = extract_err.into();
333
334        assert!(matches!(main_err, Error::Extraction(ExtractError::Io(_))));
335        assert!(main_err.to_string().contains("extraction error"));
336    }
337
338    #[test]
339    fn test_result_type_alias() {
340        fn example_function() -> Result<i32> {
341            Ok(42)
342        }
343
344        fn failing_function() -> Result<i32> {
345            Err(Error::Other("test failure".to_string()))
346        }
347
348        assert!(example_function().is_ok());
349        assert!(failing_function().is_err());
350    }
351
352    // ========== All Modality Variants ==========
353
354    #[test]
355    fn test_embed_error_all_modalities() {
356        let text_err = EmbedError::ModalityNotSupported(Modality::Text);
357        assert!(text_err.to_string().contains("Text"));
358
359        let image_err = EmbedError::ModalityNotSupported(Modality::Image);
360        assert!(image_err.to_string().contains("Image"));
361
362        let audio_err = EmbedError::ModalityNotSupported(Modality::Audio);
363        assert!(audio_err.to_string().contains("Audio"));
364    }
365}