Skip to main content

ocas_core/
error.rs

1//! Unified error types for oCAS.
2
3use thiserror::Error;
4
5/// The primary error type returned by oCAS operations.
6///
7/// # Example
8///
9/// ```
10/// use ocas_core::error::OcasError;
11///
12/// let err = OcasError::ParseError {
13///     message: "unexpected token".into(),
14///     span: Some((0, 3)),
15/// };
16/// assert_eq!(err.to_string(), "parse error at bytes 0..3: unexpected token");
17/// ```
18#[derive(Debug, Clone, PartialEq, Error)]
19#[non_exhaustive]
20pub enum OcasError {
21    /// A parsing error with an optional source span.
22    #[error("parse error{}: {message}", match span {
23        Some((start, end)) => format!(" at bytes {start}..{end}"),
24        None => String::new(),
25    })]
26    ParseError {
27        /// Human-readable error message.
28        message: String,
29        /// Optional byte offset into the source string.
30        span: Option<(usize, usize)>,
31    },
32
33    /// An operation was requested on an incompatible domain.
34    #[error("domain error: expected {expected}, found {found}")]
35    DomainError {
36        /// Expected domain or type.
37        expected: String,
38        /// Actual value or type encountered.
39        found: String,
40    },
41
42    /// A numeric overflow or underflow occurred.
43    #[error("numeric overflow")]
44    NumericOverflow,
45
46    /// The requested operation is not yet implemented or supported.
47    #[error("unsupported operation: {message}")]
48    UnsupportedOperation {
49        /// Description of the unsupported operation.
50        message: String,
51    },
52
53    /// A backend library returned an error.
54    #[error("backend error ({backend}): {message}")]
55    BackendError {
56        /// Name of the backend.
57        backend: String,
58        /// Backend-specific error message.
59        message: String,
60    },
61
62    /// An invalid argument was supplied.
63    #[error("invalid argument '{name}': {reason}")]
64    InvalidArgument {
65        /// Name of the argument.
66        name: String,
67        /// Reason the argument is invalid.
68        reason: String,
69    },
70}
71
72/// A convenient result type for oCAS operations.
73pub type Result<T> = std::result::Result<T, OcasError>;
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use proptest::prelude::*;
79
80    mod simple {
81        use super::*;
82
83        #[test]
84        fn display_parse_error_with_span() {
85            let err = OcasError::ParseError {
86                message: "unexpected token".into(),
87                span: Some((0, 3)),
88            };
89            assert_eq!(
90                err.to_string(),
91                "parse error at bytes 0..3: unexpected token"
92            );
93        }
94
95        #[test]
96        fn display_parse_error_without_span() {
97            let err = OcasError::ParseError {
98                message: "unexpected end of input".into(),
99                span: None,
100            };
101            assert_eq!(err.to_string(), "parse error: unexpected end of input");
102        }
103
104        #[test]
105        fn display_numeric_overflow() {
106            let err = OcasError::NumericOverflow;
107            assert_eq!(err.to_string(), "numeric overflow");
108        }
109
110        #[test]
111        fn display_domain_error() {
112            let err = OcasError::DomainError {
113                expected: "integer".into(),
114                found: "rational".into(),
115            };
116            assert_eq!(
117                err.to_string(),
118                "domain error: expected integer, found rational"
119            );
120        }
121    }
122
123    mod medium {
124        use super::*;
125
126        #[test]
127        fn display_unsupported_operation() {
128            let err = OcasError::UnsupportedOperation {
129                message: "symbolic integration not implemented".into(),
130            };
131            assert_eq!(
132                err.to_string(),
133                "unsupported operation: symbolic integration not implemented"
134            );
135        }
136
137        #[test]
138        fn display_backend_error() {
139            let err = OcasError::BackendError {
140                backend: "gmp".into(),
141                message: "division by zero".into(),
142            };
143            assert_eq!(err.to_string(), "backend error (gmp): division by zero");
144        }
145
146        #[test]
147        fn display_invalid_argument() {
148            let err = OcasError::InvalidArgument {
149                name: "threads".into(),
150                reason: "must be greater than zero".into(),
151            };
152            assert_eq!(
153                err.to_string(),
154                "invalid argument 'threads': must be greater than zero"
155            );
156        }
157
158        #[test]
159        fn error_implements_std_error() {
160            let err = OcasError::NumericOverflow;
161            let dyn_err: &dyn std::error::Error = &err;
162            assert_eq!(dyn_err.to_string(), "numeric overflow");
163        }
164    }
165
166    mod complex {
167        use super::*;
168
169        #[test]
170        fn error_clone_and_equality() {
171            let err = OcasError::ParseError {
172                message: "unexpected token".into(),
173                span: Some((0, 3)),
174            };
175            let cloned = err.clone();
176            assert_eq!(err, cloned);
177        }
178
179        #[test]
180        fn result_type_alias_compiles() {
181            fn returns_result() -> Result<i32> {
182                Ok(42)
183            }
184            assert_eq!(returns_result().unwrap(), 42);
185        }
186
187        #[test]
188        fn all_variants_round_trip_through_display() {
189            let errors: Vec<OcasError> = vec![
190                OcasError::ParseError {
191                    message: "m".into(),
192                    span: Some((1, 2)),
193                },
194                OcasError::ParseError {
195                    message: "m".into(),
196                    span: None,
197                },
198                OcasError::DomainError {
199                    expected: "e".into(),
200                    found: "f".into(),
201                },
202                OcasError::NumericOverflow,
203                OcasError::UnsupportedOperation {
204                    message: "u".into(),
205                },
206                OcasError::BackendError {
207                    backend: "b".into(),
208                    message: "m".into(),
209                },
210                OcasError::InvalidArgument {
211                    name: "n".into(),
212                    reason: "r".into(),
213                },
214            ];
215            for err in errors {
216                assert!(!err.to_string().is_empty());
217                assert_eq!(err.clone(), err);
218            }
219        }
220    }
221
222    mod extreme {
223        use super::*;
224
225        proptest! {
226            #[test]
227            fn unsupported_operation_display_contains_message(message in "[a-zA-Z0-9_ ]{1,64}") {
228                let err = OcasError::UnsupportedOperation { message: message.clone() };
229                let text = err.to_string();
230                prop_assert!(!text.is_empty());
231                prop_assert!(text.contains(&message), "{text} should contain {message}");
232            }
233
234            #[test]
235            fn parse_error_span_is_in_display((start, end) in (0usize..1000, 0usize..1000)) {
236                let err = OcasError::ParseError {
237                    message: "test".into(),
238                    span: Some((start, end)),
239                };
240                let text = err.to_string();
241                prop_assert!(text.contains("parse error"));
242            }
243        }
244    }
245}