1use thiserror::Error;
8
9#[derive(Error, Debug)]
11pub enum CtxError {
12 #[error("Database error: {0}")]
15 Database(#[from] rusqlite::Error),
16
17 #[cfg(feature = "duckdb")]
19 #[error("Analytics error: {0}")]
20 Analytics(#[from] duckdb::Error),
21
22 #[error("IO error: {0}")]
25 Io(#[from] std::io::Error),
26
27 #[error("Readline error: {0}")]
29 Readline(#[from] rustyline::error::ReadlineError),
30
31 #[error("Serialization error: {0}")]
34 Serialization(#[from] serde_json::Error),
35
36 #[error("Network error: {0}")]
39 Network(#[from] reqwest::Error),
40
41 #[error("Embedding error: {0}")]
44 Embedding(String),
45
46 #[error("Rate limited: retry after {0} seconds")]
48 RateLimited(u64),
49
50 #[error("Invalid API key")]
52 InvalidApiKey,
53
54 #[error("Model not found: {0}")]
56 ModelNotFound(String),
57
58 #[error("Dimension mismatch: expected {expected}, got {actual}")]
60 DimensionMismatch { expected: usize, actual: usize },
61
62 #[error("Git error: {0}")]
65 Git(String),
66
67 #[error("Not a git repository")]
69 NotGitRepo,
70
71 #[error("Invalid revision: {0}")]
73 InvalidRevision(String),
74
75 #[error("No changes found")]
77 NoChanges,
78
79 #[error("No relevant files found")]
82 NoMatches,
83
84 #[error("Index not found: {0}")]
86 #[allow(dead_code)] IndexNotFound(String),
88
89 #[error("Index schema version mismatch (found v{found}, expected v{expected}). Run 'ctx index --force' to rebuild the index.")]
91 SchemaVersionMismatch { found: i64, expected: i64 },
92
93 #[error("Parse error: {0}")]
95 #[allow(dead_code)] ParseError(String),
97
98 #[error("Token counting error: {0}")]
100 #[allow(dead_code)] TokenCount(String),
102
103 #[error("Failed to read file '{path}': {message}")]
105 #[allow(dead_code)] FileRead { path: String, message: String },
107
108 #[error("{0}")]
111 Other(String),
112}
113
114pub type Result<T> = std::result::Result<T, CtxError>;
116
117impl CtxError {
118 pub fn embedding(msg: impl Into<String>) -> Self {
120 CtxError::Embedding(msg.into())
121 }
122
123 pub fn git(msg: impl Into<String>) -> Self {
125 CtxError::Git(msg.into())
126 }
127
128 #[allow(dead_code)] pub fn parse(msg: impl Into<String>) -> Self {
131 CtxError::ParseError(msg.into())
132 }
133
134 #[allow(dead_code)] pub fn token_count(msg: impl Into<String>) -> Self {
137 CtxError::TokenCount(msg.into())
138 }
139
140 #[allow(dead_code)] pub fn file_read(path: impl Into<String>, msg: impl Into<String>) -> Self {
143 CtxError::FileRead {
144 path: path.into(),
145 message: msg.into(),
146 }
147 }
148
149 #[allow(dead_code)] pub fn other(msg: impl Into<String>) -> Self {
152 CtxError::Other(msg.into())
153 }
154}
155
156impl From<String> for CtxError {
159 fn from(s: String) -> Self {
160 CtxError::Other(s)
161 }
162}
163
164impl From<&str> for CtxError {
165 fn from(s: &str) -> Self {
166 CtxError::Other(s.to_string())
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_error_display() {
176 let err = CtxError::Database(rusqlite::Error::InvalidQuery);
177 assert!(err.to_string().contains("Database error"));
178
179 let err = CtxError::DimensionMismatch {
180 expected: 1536,
181 actual: 384,
182 };
183 assert_eq!(
184 err.to_string(),
185 "Dimension mismatch: expected 1536, got 384"
186 );
187
188 let err = CtxError::RateLimited(60);
189 assert_eq!(err.to_string(), "Rate limited: retry after 60 seconds");
190 }
191
192 #[test]
193 fn test_error_constructors() {
194 let err = CtxError::embedding("test error");
195 assert!(matches!(err, CtxError::Embedding(_)));
196
197 let err = CtxError::git("not a repo");
198 assert!(matches!(err, CtxError::Git(_)));
199
200 let err = CtxError::file_read("/path/to/file", "permission denied");
201 assert!(matches!(err, CtxError::FileRead { .. }));
202 }
203
204 #[test]
205 fn test_from_string() {
206 let err: CtxError = "some error".into();
207 assert!(matches!(err, CtxError::Other(_)));
208
209 let err: CtxError = String::from("another error").into();
210 assert!(matches!(err, CtxError::Other(_)));
211 }
212}