1use thiserror::Error;
5
6#[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#[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#[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#[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#[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#[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#[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#[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#[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
253impl From<anyhow::Error> for RagError {
255 fn from(err: anyhow::Error) -> Self {
256 RagError::Other(format!("{:#}", err))
257 }
258}
259
260impl RagError {
262 pub fn other(msg: impl Into<String>) -> Self {
264 RagError::Other(msg.into())
265 }
266
267 pub fn to_user_string(&self) -> String {
269 format!("{}", self)
270 }
271
272 pub fn is_user_error(&self) -> bool {
274 matches!(
275 self,
276 RagError::Validation(_) | RagError::Config(ConfigError::InvalidValue { .. })
277 )
278 }
279
280 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}