project_rag/
error.rs

1/// Centralized error types for project-rag using thiserror
2///
3/// Provides domain-specific error types for better error handling and user-facing messages.
4use thiserror::Error;
5
6/// Main error type for the RAG system
7#[derive(Error, Debug)]
8pub enum RagError {
9    #[error("Embedding error: {0}")]
10    Embedding(#[from] EmbeddingError),
11
12    #[error("Vector database error: {0}")]
13    VectorDb(#[from] VectorDbError),
14
15    #[error("Indexing error: {0}")]
16    Indexing(#[from] IndexingError),
17
18    #[error("Chunking error: {0}")]
19    Chunking(#[from] ChunkingError),
20
21    #[error("Configuration error: {0}")]
22    Config(#[from] ConfigError),
23
24    #[error("Validation error: {0}")]
25    Validation(#[from] ValidationError),
26
27    #[error("Git error: {0}")]
28    Git(#[from] GitError),
29
30    #[error("Cache error: {0}")]
31    Cache(#[from] CacheError),
32
33    #[error("IO error: {0}")]
34    Io(#[from] std::io::Error),
35
36    #[error("{0}")]
37    Other(String),
38}
39
40/// Errors related to embedding generation
41#[derive(Error, Debug)]
42pub enum EmbeddingError {
43    #[error("Failed to initialize embedding model: {0}")]
44    InitializationFailed(String),
45
46    #[error("Failed to generate embeddings: {0}")]
47    GenerationFailed(String),
48
49    #[error("Embedding batch is empty")]
50    EmptyBatch,
51
52    #[error("Embedding generation timed out after {0} seconds")]
53    Timeout(u64),
54
55    #[error("Invalid embedding dimension: expected {expected}, got {actual}")]
56    DimensionMismatch { expected: usize, actual: usize },
57
58    #[error("Model lock was poisoned: {0}")]
59    LockPoisoned(String),
60}
61
62/// Errors related to vector database operations
63#[derive(Error, Debug)]
64pub enum VectorDbError {
65    #[error("Failed to initialize vector database: {0}")]
66    InitializationFailed(String),
67
68    #[error("Failed to connect to vector database: {0}")]
69    ConnectionFailed(String),
70
71    #[error("Failed to create collection '{collection}': {reason}")]
72    CollectionCreationFailed { collection: String, reason: String },
73
74    #[error("Collection '{0}' not found")]
75    CollectionNotFound(String),
76
77    #[error("Failed to store embeddings: {0}")]
78    StoreFailed(String),
79
80    #[error("Failed to search embeddings: {0}")]
81    SearchFailed(String),
82
83    #[error("Failed to delete embeddings: {0}")]
84    DeleteFailed(String),
85
86    #[error("Failed to get statistics: {0}")]
87    StatisticsFailed(String),
88
89    #[error("Failed to clear database: {0}")]
90    ClearFailed(String),
91
92    #[error("Invalid search parameters: {0}")]
93    InvalidSearchParams(String),
94
95    #[error("Database is not initialized")]
96    NotInitialized,
97}
98
99/// Errors related to file indexing
100#[derive(Error, Debug)]
101pub enum IndexingError {
102    #[error("Directory not found: {0}")]
103    DirectoryNotFound(String),
104
105    #[error("Path is not a directory: {0}")]
106    NotADirectory(String),
107
108    #[error("Failed to walk directory: {0}")]
109    WalkFailed(String),
110
111    #[error("Failed to read file '{file}': {reason}")]
112    FileReadFailed { file: String, reason: String },
113
114    #[error("File is not valid UTF-8: {0}")]
115    InvalidUtf8(String),
116
117    #[error("File is binary and cannot be indexed: {0}")]
118    BinaryFile(String),
119
120    #[error("File size exceeds maximum: {size} > {max}")]
121    FileTooLarge { size: usize, max: usize },
122
123    #[error("Failed to calculate file hash: {0}")]
124    HashCalculationFailed(String),
125
126    #[error("No files found to index")]
127    NoFilesFound,
128
129    #[error("Indexing was cancelled")]
130    Cancelled,
131}
132
133/// Errors related to code chunking
134#[derive(Error, Debug)]
135pub enum ChunkingError {
136    #[error("Failed to parse code: {0}")]
137    ParseFailed(String),
138
139    #[error("Unsupported language: {0}")]
140    UnsupportedLanguage(String),
141
142    #[error("Invalid chunk size: {0}")]
143    InvalidChunkSize(String),
144
145    #[error("No chunks generated from file: {0}")]
146    NoChunksGenerated(String),
147
148    #[error("AST parsing failed: {0}")]
149    AstParsingFailed(String),
150}
151
152/// Errors related to configuration
153#[derive(Error, Debug)]
154pub enum ConfigError {
155    #[error("Failed to load configuration file: {0}")]
156    LoadFailed(String),
157
158    #[error("Failed to parse configuration: {0}")]
159    ParseFailed(String),
160
161    #[error("Invalid configuration value for '{key}': {reason}")]
162    InvalidValue { key: String, reason: String },
163
164    #[error("Missing required configuration: {0}")]
165    MissingRequired(String),
166
167    #[error("Failed to save configuration: {0}")]
168    SaveFailed(String),
169
170    #[error("Configuration file not found: {0}")]
171    FileNotFound(String),
172}
173
174/// Errors related to input validation
175#[derive(Error, Debug)]
176pub enum ValidationError {
177    #[error("Path does not exist: {0}")]
178    PathNotFound(String),
179
180    #[error("Path is not absolute: {0}")]
181    PathNotAbsolute(String),
182
183    #[error("Invalid path: {0}")]
184    InvalidPath(String),
185
186    #[error("Invalid project name: {0}")]
187    InvalidProjectName(String),
188
189    #[error("Invalid pattern: {0}")]
190    InvalidPattern(String),
191
192    #[error("{field} must be {constraint}, got {actual}")]
193    ConstraintViolation {
194        field: String,
195        constraint: String,
196        actual: String,
197    },
198
199    #[error("Invalid value for {0}: {1}")]
200    InvalidValue(String, String),
201
202    #[error("Empty {0}")]
203    Empty(String),
204}
205
206/// Errors related to git operations
207#[derive(Error, Debug)]
208pub enum GitError {
209    #[error("Git repository not found at: {0}")]
210    RepoNotFound(String),
211
212    #[error("Failed to open git repository: {0}")]
213    OpenFailed(String),
214
215    #[error("Failed to get git reference: {0}")]
216    RefNotFound(String),
217
218    #[error("Failed to iterate commits: {0}")]
219    IterFailed(String),
220
221    #[error("Invalid commit hash: {0}")]
222    InvalidCommitHash(String),
223
224    #[error("Failed to parse commit: {0}")]
225    ParseFailed(String),
226
227    #[error("Branch not found: {0}")]
228    BranchNotFound(String),
229
230    #[error("No commits found matching criteria")]
231    NoCommitsFound,
232}
233
234/// Errors related to cache operations
235#[derive(Error, Debug)]
236pub enum CacheError {
237    #[error("Failed to load cache from '{path}': {reason}")]
238    LoadFailed { path: String, reason: String },
239
240    #[error("Failed to save cache to '{path}': {reason}")]
241    SaveFailed { path: String, reason: String },
242
243    #[error("Failed to parse cache file: {0}")]
244    ParseFailed(String),
245
246    #[error("Cache is corrupted: {0}")]
247    Corrupted(String),
248
249    #[error("Failed to create cache directory: {0}")]
250    DirectoryCreationFailed(String),
251}
252
253// Conversion from anyhow::Error to RagError
254impl From<anyhow::Error> for RagError {
255    fn from(err: anyhow::Error) -> Self {
256        RagError::Other(format!("{:#}", err))
257    }
258}
259
260// Helper methods for RagError
261impl RagError {
262    /// Create a new error from a string message
263    pub fn other(msg: impl Into<String>) -> Self {
264        RagError::Other(msg.into())
265    }
266
267    /// Convert to a user-facing error string suitable for MCP responses
268    pub fn to_user_string(&self) -> String {
269        format!("{}", self)
270    }
271
272    /// Check if this is a user error (validation, not found) vs system error
273    pub fn is_user_error(&self) -> bool {
274        matches!(
275            self,
276            RagError::Validation(_) | RagError::Config(ConfigError::InvalidValue { .. })
277        )
278    }
279
280    /// Check if this error is retryable
281    pub fn is_retryable(&self) -> bool {
282        matches!(
283            self,
284            RagError::VectorDb(VectorDbError::ConnectionFailed(_))
285                | RagError::Embedding(EmbeddingError::Timeout(_))
286                | RagError::Io(_)
287        )
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_error_display() {
297        let err = RagError::Validation(ValidationError::PathNotFound("/test".to_string()));
298        assert_eq!(
299            err.to_string(),
300            "Validation error: Path does not exist: /test"
301        );
302    }
303
304    #[test]
305    fn test_error_from_io() {
306        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
307        let rag_err: RagError = io_err.into();
308        assert!(matches!(rag_err, RagError::Io(_)));
309    }
310
311    #[test]
312    fn test_error_from_anyhow() {
313        let anyhow_err = anyhow::anyhow!("test error");
314        let rag_err: RagError = anyhow_err.into();
315        assert!(matches!(rag_err, RagError::Other(_)));
316    }
317
318    #[test]
319    fn test_is_user_error() {
320        let user_err = RagError::Validation(ValidationError::InvalidPath("test".to_string()));
321        assert!(user_err.is_user_error());
322
323        let system_err = RagError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
324        assert!(!system_err.is_user_error());
325    }
326
327    #[test]
328    fn test_is_retryable() {
329        let retryable = RagError::VectorDb(VectorDbError::ConnectionFailed("test".to_string()));
330        assert!(retryable.is_retryable());
331
332        let not_retryable = RagError::Validation(ValidationError::InvalidPath("test".to_string()));
333        assert!(!not_retryable.is_retryable());
334    }
335
336    #[test]
337    fn test_embedding_error_timeout() {
338        let err = EmbeddingError::Timeout(30);
339        assert_eq!(
340            err.to_string(),
341            "Embedding generation timed out after 30 seconds"
342        );
343    }
344
345    #[test]
346    fn test_embedding_error_dimension_mismatch() {
347        let err = EmbeddingError::DimensionMismatch {
348            expected: 384,
349            actual: 512,
350        };
351        assert_eq!(
352            err.to_string(),
353            "Invalid embedding dimension: expected 384, got 512"
354        );
355    }
356
357    #[test]
358    fn test_vector_db_error_collection_creation() {
359        let err = VectorDbError::CollectionCreationFailed {
360            collection: "test_collection".to_string(),
361            reason: "already exists".to_string(),
362        };
363        assert_eq!(
364            err.to_string(),
365            "Failed to create collection 'test_collection': already exists"
366        );
367    }
368
369    #[test]
370    fn test_indexing_error_file_too_large() {
371        let err = IndexingError::FileTooLarge {
372            size: 1000000,
373            max: 500000,
374        };
375        assert_eq!(
376            err.to_string(),
377            "File size exceeds maximum: 1000000 > 500000"
378        );
379    }
380
381    #[test]
382    fn test_validation_error_constraint() {
383        let err = ValidationError::ConstraintViolation {
384            field: "max_file_size".to_string(),
385            constraint: "less than 100MB".to_string(),
386            actual: "200MB".to_string(),
387        };
388        assert_eq!(
389            err.to_string(),
390            "max_file_size must be less than 100MB, got 200MB"
391        );
392    }
393
394    #[test]
395    fn test_config_error_invalid_value() {
396        let err = ConfigError::InvalidValue {
397            key: "port".to_string(),
398            reason: "must be between 1-65535".to_string(),
399        };
400        assert_eq!(
401            err.to_string(),
402            "Invalid configuration value for 'port': must be between 1-65535"
403        );
404    }
405
406    #[test]
407    fn test_cache_error_load_failed() {
408        let err = CacheError::LoadFailed {
409            path: "/tmp/cache.json".to_string(),
410            reason: "permission denied".to_string(),
411        };
412        assert_eq!(
413            err.to_string(),
414            "Failed to load cache from '/tmp/cache.json': permission denied"
415        );
416    }
417
418    #[test]
419    fn test_rag_error_other() {
420        let err = RagError::other("custom error message");
421        assert_eq!(err.to_string(), "custom error message");
422    }
423
424    #[test]
425    fn test_error_chain() {
426        let embedding_err = EmbeddingError::GenerationFailed("model error".to_string());
427        let rag_err: RagError = embedding_err.into();
428        assert!(matches!(rag_err, RagError::Embedding(_)));
429        assert_eq!(
430            rag_err.to_string(),
431            "Embedding error: Failed to generate embeddings: model error"
432        );
433    }
434}