eidetica/backend/
errors.rs1use thiserror::Error;
7
8use crate::entry::ID;
9
10#[non_exhaustive]
19#[derive(Debug, Error)]
20pub enum BackendError {
21 #[error("Entry not found: {id}")]
23 EntryNotFound {
24 id: ID,
26 },
27
28 #[error("Entry {entry_id} failed validation: {reason}")]
30 EntryValidationFailed {
31 entry_id: ID,
33 reason: String,
35 },
36
37 #[error("Verification status not found for entry: {id}")]
39 VerificationStatusNotFound {
40 id: ID,
42 },
43
44 #[error("Entry {entry_id} is not in tree {tree_id}")]
46 EntryNotInTree {
47 entry_id: ID,
49 tree_id: ID,
51 },
52
53 #[error("Entry {entry_id} is not in subtree {subtree} of tree {tree_id}")]
55 EntryNotInSubtree {
56 entry_id: ID,
58 tree_id: ID,
60 subtree: String,
62 },
63
64 #[error("Cycle detected in DAG while traversing from {entry_id}")]
66 CycleDetected {
67 entry_id: ID,
69 },
70
71 #[error("No common ancestor found for entries: {entry_ids:?}")]
73 NoCommonAncestor {
74 entry_ids: Vec<ID>,
76 },
77
78 #[error("No entry IDs provided for {operation}")]
80 EmptyEntryList {
81 operation: String,
83 },
84
85 #[error("Height calculation corruption: {reason}")]
87 HeightCalculationCorruption {
88 reason: String,
90 },
91
92 #[error("Private key not found: {key_name}")]
94 PrivateKeyNotFound {
95 key_name: String,
97 },
98
99 #[error("Serialization failed")]
101 SerializationFailed {
102 #[source]
104 source: serde_json::Error,
105 },
106
107 #[error("Deserialization failed")]
109 DeserializationFailed {
110 #[source]
112 source: serde_json::Error,
113 },
114
115 #[error("File I/O error")]
117 FileIo {
118 #[source]
120 source: std::io::Error,
121 },
122
123 #[error("CRDT cache operation failed: {reason}")]
125 CrdtCacheError {
126 reason: String,
128 },
129
130 #[error("Database integrity violation: {reason}")]
132 TreeIntegrityViolation {
133 reason: String,
135 },
136
137 #[error("Invalid tree reference: {tree_id}")]
139 InvalidTreeReference {
140 tree_id: String,
142 },
143
144 #[error("Database state inconsistency: {reason}")]
146 StateInconsistency {
147 reason: String,
149 },
150
151 #[error("Cache operation failed: {reason}")]
153 CacheError {
154 reason: String,
156 },
157
158 #[cfg(any(feature = "sqlite", feature = "postgres"))]
160 #[error("SQL error: {reason}")]
161 SqlxError {
162 reason: String,
164 #[source]
166 source: Option<sqlx::Error>,
167 },
168}
169
170impl BackendError {
171 pub fn is_not_found(&self) -> bool {
173 matches!(
174 self,
175 BackendError::EntryNotFound { .. }
176 | BackendError::VerificationStatusNotFound { .. }
177 | BackendError::PrivateKeyNotFound { .. }
178 )
179 }
180
181 pub fn is_integrity_error(&self) -> bool {
183 matches!(
184 self,
185 BackendError::EntryValidationFailed { .. }
186 | BackendError::CycleDetected { .. }
187 | BackendError::HeightCalculationCorruption { .. }
188 | BackendError::TreeIntegrityViolation { .. }
189 | BackendError::StateInconsistency { .. }
190 )
191 }
192
193 pub fn is_io_error(&self) -> bool {
195 #[cfg(any(feature = "sqlite", feature = "postgres"))]
196 if matches!(self, BackendError::SqlxError { .. }) {
197 return true;
198 }
199 matches!(
200 self,
201 BackendError::FileIo { .. }
202 | BackendError::SerializationFailed { .. }
203 | BackendError::DeserializationFailed { .. }
204 )
205 }
206
207 #[cfg(any(feature = "sqlite", feature = "postgres"))]
209 pub fn is_sql_error(&self) -> bool {
210 matches!(self, BackendError::SqlxError { .. })
211 }
212
213 pub fn is_cache_error(&self) -> bool {
215 matches!(
216 self,
217 BackendError::CrdtCacheError { .. } | BackendError::CacheError { .. }
218 )
219 }
220
221 pub fn is_logical_error(&self) -> bool {
223 matches!(
224 self,
225 BackendError::EntryNotInTree { .. }
226 | BackendError::EntryNotInSubtree { .. }
227 | BackendError::NoCommonAncestor { .. }
228 | BackendError::EmptyEntryList { .. }
229 )
230 }
231
232 pub fn entry_id(&self) -> Option<&ID> {
234 match self {
235 BackendError::EntryNotFound { id }
236 | BackendError::VerificationStatusNotFound { id }
237 | BackendError::EntryValidationFailed { entry_id: id, .. }
238 | BackendError::CycleDetected { entry_id: id }
239 | BackendError::EntryNotInTree { entry_id: id, .. }
240 | BackendError::EntryNotInSubtree { entry_id: id, .. } => Some(id),
241 _ => None,
242 }
243 }
244
245 pub fn tree_id(&self) -> Option<String> {
247 match self {
248 BackendError::EntryNotInTree { tree_id, .. }
249 | BackendError::EntryNotInSubtree { tree_id, .. } => Some(tree_id.to_string()),
250 BackendError::InvalidTreeReference { tree_id } => Some(tree_id.clone()),
251 _ => None,
252 }
253 }
254}
255
256impl From<BackendError> for crate::Error {
258 fn from(err: BackendError) -> Self {
259 crate::Error::Backend(err)
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_error_helpers() {
270 let err = BackendError::EntryNotFound {
271 id: ID::from("test-entry"),
272 };
273 assert!(err.is_not_found());
274 assert_eq!(err.entry_id(), Some(&ID::from("test-entry")));
275
276 let err = BackendError::CycleDetected {
277 entry_id: ID::from("cycle-entry"),
278 };
279 assert!(err.is_integrity_error());
280 assert_eq!(err.entry_id(), Some(&ID::from("cycle-entry")));
281
282 let err = BackendError::FileIo {
283 source: std::io::Error::new(std::io::ErrorKind::NotFound, "test"),
284 };
285 assert!(err.is_io_error());
286
287 let err = BackendError::CacheError {
288 reason: "test".to_string(),
289 };
290 assert!(err.is_cache_error());
291
292 let err = BackendError::EmptyEntryList {
293 operation: "test".to_string(),
294 };
295 assert!(err.is_logical_error());
296 }
297
298 #[test]
299 fn test_error_conversion() {
300 let db_err = BackendError::EntryNotFound {
301 id: ID::from("test"),
302 };
303 let err: crate::Error = db_err.into();
304 match err {
305 crate::Error::Backend(BackendError::EntryNotFound { id }) => {
306 assert_eq!(id.to_string(), "test")
307 }
308 _ => panic!("Unexpected error variant"),
309 }
310 }
311}