Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/crdt/
errors.rs

1//! Error types for CRDT operations.
2//!
3//! This module defines structured error types specific to CRDT (Conflict-free Replicated Data Type)
4//! operations, providing detailed context for merge failures, serialization issues,
5//! and type mismatches that can occur during CRDT operations.
6
7use thiserror::Error;
8
9use crate::crdt::doc::PathError;
10
11/// Structured error types for CRDT operations.
12///
13/// This enum provides specific error variants for different types of failures
14/// that can occur during CRDT operations, including merge conflicts, serialization
15/// issues, and type validation errors.
16#[non_exhaustive]
17#[derive(Debug, Error)]
18pub enum CRDTError {
19    /// A merge operation failed between two CRDT instances
20    #[error("CRDT merge failed: {reason}")]
21    MergeFailed { reason: String },
22
23    /// Serialization of CRDT data failed
24    #[error("CRDT serialization failed: {reason}")]
25    SerializationFailed { reason: String },
26
27    /// Deserialization of CRDT data failed
28    #[error("CRDT deserialization failed: {reason}")]
29    DeserializationFailed { reason: String },
30
31    /// Type mismatch during CRDT operation
32    #[error("CRDT type mismatch: expected {expected}, found {actual}")]
33    TypeMismatch { expected: String, actual: String },
34
35    /// Invalid value provided for CRDT operation
36    #[error("Invalid CRDT value: {reason}")]
37    InvalidValue { reason: String },
38
39    /// List operation failed
40    #[error("CRDT list operation failed: {operation} - {reason}")]
41    ListOperationFailed { operation: String, reason: String },
42
43    /// Document operation failed
44    #[error("CRDT document operation failed: {operation} - {reason}")]
45    DocOperationFailed { operation: String, reason: String },
46
47    /// Nested structure operation failed
48    #[error("CRDT nested operation failed: {path} - {reason}")]
49    NestedOperationFailed { path: String, reason: String },
50
51    /// Invalid UUID format in list operations
52    #[error("Invalid UUID format: {uuid}")]
53    InvalidUuid { uuid: String },
54
55    /// Element not found in CRDT structure
56    #[error("CRDT element not found: {key}")]
57    ElementNotFound { key: String },
58
59    /// Invalid path for nested operations
60    #[error("Invalid CRDT path: {path}")]
61    InvalidPath { path: String },
62
63    /// List index out of bounds
64    #[error("List index out of bounds: index {index}, length {len}")]
65    ListIndexOutOfBounds { index: usize, len: usize },
66}
67
68impl CRDTError {
69    /// Check if this error is related to merge operations
70    pub fn is_merge_error(&self) -> bool {
71        matches!(self, CRDTError::MergeFailed { .. })
72    }
73
74    /// Check if this error is related to serialization
75    pub fn is_serialization_error(&self) -> bool {
76        matches!(
77            self,
78            CRDTError::SerializationFailed { .. } | CRDTError::DeserializationFailed { .. }
79        )
80    }
81
82    /// Check if this error is related to type mismatches
83    pub fn is_type_error(&self) -> bool {
84        matches!(self, CRDTError::TypeMismatch { .. })
85    }
86
87    /// Check if this error is related to list operations (operation-level)
88    pub fn is_list_operation_error(&self) -> bool {
89        matches!(self, CRDTError::ListOperationFailed { .. })
90    }
91
92    /// Check if this error is related to doc operations
93    pub fn is_doc_error(&self) -> bool {
94        matches!(self, CRDTError::DocOperationFailed { .. })
95    }
96
97    /// Check if this error is related to nested operations
98    pub fn is_nested_error(&self) -> bool {
99        matches!(self, CRDTError::NestedOperationFailed { .. })
100    }
101
102    /// Check if this error is related to element lookup
103    pub fn is_not_found(&self) -> bool {
104        matches!(self, CRDTError::ElementNotFound { .. })
105    }
106
107    /// Check if this error is related to list operations
108    pub fn is_list_error(&self) -> bool {
109        matches!(self, CRDTError::ListIndexOutOfBounds { .. })
110    }
111
112    /// Get the operation type if this is an operation-specific error
113    pub fn operation(&self) -> Option<&str> {
114        match self {
115            CRDTError::ListOperationFailed { operation, .. }
116            | CRDTError::DocOperationFailed { operation, .. } => Some(operation),
117            _ => None,
118        }
119    }
120
121    /// Get the path if this is a path-related error
122    pub fn path(&self) -> Option<&str> {
123        match self {
124            CRDTError::NestedOperationFailed { path, .. } | CRDTError::InvalidPath { path } => {
125                Some(path)
126            }
127            _ => None,
128        }
129    }
130
131    /// Get the key if this is a key-related error
132    pub fn key(&self) -> Option<&str> {
133        match self {
134            CRDTError::ElementNotFound { key } => Some(key),
135            _ => None,
136        }
137    }
138}
139
140// Conversion from PathError to CRDTError
141impl From<PathError> for CRDTError {
142    fn from(err: PathError) -> Self {
143        CRDTError::InvalidPath {
144            path: err.to_string(),
145        }
146    }
147}
148
149// Conversion from CRDTError to the main Error type
150impl From<CRDTError> for crate::Error {
151    fn from(err: CRDTError) -> Self {
152        crate::Error::CRDT(err)
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_crdt_error_list_index_out_of_bounds() {
162        let error = CRDTError::ListIndexOutOfBounds { index: 5, len: 3 };
163
164        assert!(error.is_list_error());
165        assert!(!error.is_merge_error());
166        assert!(!error.is_serialization_error());
167        assert!(!error.is_type_error());
168        assert!(!error.is_list_operation_error());
169        assert!(!error.is_doc_error());
170        assert!(!error.is_nested_error());
171        assert!(!error.is_not_found());
172
173        assert_eq!(error.operation(), None);
174        assert_eq!(error.path(), None);
175        assert_eq!(error.key(), None);
176
177        let display = format!("{error}");
178        assert!(display.contains("List index out of bounds"));
179        assert!(display.contains("index 5"));
180        assert!(display.contains("length 3"));
181    }
182
183    #[test]
184    fn test_crdt_error_classification() {
185        let merge_error = CRDTError::MergeFailed {
186            reason: "test".to_string(),
187        };
188        assert!(merge_error.is_merge_error());
189
190        let serialization_error = CRDTError::SerializationFailed {
191            reason: "test".to_string(),
192        };
193        assert!(serialization_error.is_serialization_error());
194
195        let type_error = CRDTError::TypeMismatch {
196            expected: "string".to_string(),
197            actual: "int".to_string(),
198        };
199        assert!(type_error.is_type_error());
200
201        let list_error = CRDTError::ListOperationFailed {
202            operation: "insert".to_string(),
203            reason: "test".to_string(),
204        };
205        assert!(list_error.is_list_operation_error());
206        assert_eq!(list_error.operation(), Some("insert"));
207
208        let doc_error = CRDTError::DocOperationFailed {
209            operation: "set".to_string(),
210            reason: "test".to_string(),
211        };
212        assert!(doc_error.is_doc_error());
213        assert_eq!(doc_error.operation(), Some("set"));
214
215        let nested_error = CRDTError::NestedOperationFailed {
216            path: "user.profile".to_string(),
217            reason: "test".to_string(),
218        };
219        assert!(nested_error.is_nested_error());
220        assert_eq!(nested_error.path(), Some("user.profile"));
221
222        let not_found_error = CRDTError::ElementNotFound {
223            key: "missing".to_string(),
224        };
225        assert!(not_found_error.is_not_found());
226        assert_eq!(not_found_error.key(), Some("missing"));
227    }
228
229    #[test]
230    fn test_crdt_error_conversion_to_main_error() {
231        let crdt_error = CRDTError::ListIndexOutOfBounds { index: 1, len: 0 };
232        let main_error: crate::Error = crdt_error.into();
233
234        assert_eq!(main_error.module(), "crdt");
235
236        if let crate::Error::CRDT(inner) = main_error {
237            assert!(inner.is_list_error());
238        } else {
239            panic!("Expected CRDT error variant");
240        }
241    }
242
243    #[test]
244    fn test_crdt_error_display_messages() {
245        let errors = vec![
246            CRDTError::ListIndexOutOfBounds { index: 10, len: 5 },
247            CRDTError::MergeFailed {
248                reason: "conflict".to_string(),
249            },
250            CRDTError::TypeMismatch {
251                expected: "Doc".to_string(),
252                actual: "Text".to_string(),
253            },
254            CRDTError::InvalidPath {
255                path: "invalid..path".to_string(),
256            },
257            CRDTError::ElementNotFound {
258                key: "nonexistent".to_string(),
259            },
260        ];
261
262        for error in errors {
263            let display = format!("{error}");
264            assert!(!display.is_empty());
265            assert!(display.len() > 10); // Should have meaningful error messages
266        }
267    }
268
269    #[test]
270    fn test_path_error_conversion() {
271        let path_error = PathError::InvalidComponent {
272            component: "user.name".to_string(),
273            reason: "components cannot contain dots".to_string(),
274        };
275        let crdt_error: CRDTError = path_error.into();
276
277        match crdt_error {
278            CRDTError::InvalidPath { path } => {
279                assert!(path.contains("components cannot contain dots"));
280            }
281            _ => panic!("Expected InvalidPath variant"),
282        }
283
284        // Test conversion through main Error type
285        let path_error = PathError::InvalidComponent {
286            component: "test.value".to_string(),
287            reason: "components cannot contain dots".to_string(),
288        };
289        let main_error: crate::Error = CRDTError::from(path_error).into();
290        assert_eq!(main_error.module(), "crdt");
291    }
292}