Skip to main content

celestial_core/
errors.rs

1//! Error types for astronomical calculations.
2//!
3//! This module provides a unified error type [`AstroError`] that covers the failure
4//! modes encountered in astronomical computations: invalid dates, numerical issues,
5//! external library failures, data access problems, and calculation failures.
6//!
7//! # Error Categories
8//!
9//! | Variant | Use Case | Recoverable? |
10//! |---------|----------|--------------|
11//! | [`InvalidDate`](AstroError::InvalidDate) | Calendar validation failures | No |
12//! | [`MathError`](AstroError::MathError) | Overflow, precision loss, division by zero | No |
13//! | [`ExternalLibraryError`](AstroError::ExternalLibraryError) | FFI or driver failures | No |
14//! | [`DataError`](AstroError::DataError) | File I/O, network, parsing | Yes |
15//! | [`CalculationError`](AstroError::CalculationError) | Algorithm failures | No |
16//!
17//! # Usage
18//!
19//! Most functions return [`AstroResult<T>`], which is `Result<T, AstroError>`.
20//! Use the constructor methods for consistent error creation:
21//!
22//! ```
23//! use celestial_core::{AstroError, MathErrorKind};
24//!
25//! fn safe_divide(a: f64, b: f64) -> Result<f64, AstroError> {
26//!     if b == 0.0 {
27//!         return Err(AstroError::math_error(
28//!             "safe_divide",
29//!             MathErrorKind::DivisionByZero,
30//!             "divisor is zero",
31//!         ));
32//!     }
33//!     Ok(a / b)
34//! }
35//! ```
36
37use thiserror::Error;
38
39/// Classification of mathematical errors.
40///
41/// Used with [`AstroError::MathError`] to distinguish between different
42/// numerical failure modes.
43#[derive(Debug, Clone, PartialEq)]
44pub enum MathErrorKind {
45    /// Result exceeds representable range (too large).
46    Overflow,
47    /// Result below representable range (too small/negative).
48    Underflow,
49    /// Accumulated floating-point error exceeds acceptable threshold.
50    PrecisionLoss,
51    /// Attempted division by zero or near-zero value.
52    DivisionByZero,
53    /// Input value is invalid for the operation.
54    InvalidInput,
55    /// Result is NaN or infinity.
56    NotFinite,
57    /// Value outside valid domain (e.g., latitude > 90°).
58    OutOfRange,
59}
60
61/// Unified error type for astronomical calculations.
62///
63/// Covers calendar validation, numerical issues, external dependencies,
64/// data access, and algorithmic failures. Use the constructor methods
65/// ([`invalid_date`](Self::invalid_date), [`math_error`](Self::math_error), etc.)
66/// for consistent error creation.
67#[derive(Error, Debug)]
68pub enum AstroError {
69    /// Invalid calendar date (e.g., February 30, month 13).
70    #[error("Invalid date {year}-{month:02}-{day:02}: {message}")]
71    InvalidDate {
72        year: i32,
73        month: i32,
74        day: i32,
75        message: String,
76    },
77
78    /// Numerical computation failure.
79    #[error("Math error in {operation} ({kind:?}): {message}")]
80    MathError {
81        operation: String,
82        kind: MathErrorKind,
83        message: String,
84    },
85
86    /// Failure in external library or hardware driver.
87    #[error("External library error in {function}: status {status_code} - {message}")]
88    ExternalLibraryError {
89        function: String,
90        status_code: i32,
91        message: String,
92    },
93
94    /// Data access failure (file I/O, network, parsing).
95    ///
96    /// This is the only recoverable error variant — retry or fallback may succeed.
97    #[error("Data error ({file_type} - {operation}): {message}")]
98    DataError {
99        file_type: String,
100        operation: String,
101        message: String,
102    },
103
104    /// Algorithm or calculation failure.
105    #[error("Calculation error in {context}: {message}")]
106    CalculationError { context: String, message: String },
107}
108
109/// Convenience alias for `Result<T, AstroError>`.
110pub type AstroResult<T> = Result<T, AstroError>;
111
112impl AstroError {
113    /// Creates an [`InvalidDate`](Self::InvalidDate) error.
114    pub fn invalid_date(year: i32, month: i32, day: i32, reason: &str) -> Self {
115        Self::InvalidDate {
116            year,
117            month,
118            day,
119            message: reason.to_string(),
120        }
121    }
122
123    /// Creates a [`MathError`](Self::MathError) with the given kind.
124    pub fn math_error(operation: &str, kind: MathErrorKind, reason: &str) -> Self {
125        Self::MathError {
126            operation: operation.to_string(),
127            kind,
128            message: reason.to_string(),
129        }
130    }
131
132    /// Creates an [`ExternalLibraryError`](Self::ExternalLibraryError).
133    pub fn external_library_error(function: &str, status_code: i32, message: &str) -> Self {
134        Self::ExternalLibraryError {
135            function: function.to_string(),
136            status_code,
137            message: message.to_string(),
138        }
139    }
140
141    /// Creates a [`DataError`](Self::DataError) (the only recoverable variant).
142    pub fn data_error(file_type: &str, operation: &str, reason: &str) -> Self {
143        Self::DataError {
144            file_type: file_type.to_string(),
145            operation: operation.to_string(),
146            message: reason.to_string(),
147        }
148    }
149
150    /// Creates a [`CalculationError`](Self::CalculationError).
151    pub fn calculation_error(context: &str, reason: &str) -> Self {
152        Self::CalculationError {
153            context: context.to_string(),
154            message: reason.to_string(),
155        }
156    }
157
158    /// Returns `true` if retrying or using a fallback might succeed.
159    ///
160    /// Only [`DataError`](Self::DataError) is recoverable (network retry, alternate source).
161    pub fn is_recoverable(&self) -> bool {
162        match self {
163            Self::DataError { .. } => true,
164            Self::InvalidDate { .. } => false,
165            _ => false,
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_invalid_date_error() {
176        let err = AstroError::invalid_date(2000, 13, 1, "month out of range");
177        assert_eq!(
178            err.to_string(),
179            "Invalid date 2000-13-01: month out of range"
180        );
181    }
182
183    #[test]
184    fn test_math_error_with_kind() {
185        let err = AstroError::math_error(
186            "nanosecond addition",
187            MathErrorKind::Overflow,
188            "value too large",
189        );
190        assert!(err.to_string().contains("Math error"));
191        assert!(err.to_string().contains("Overflow"));
192    }
193
194    #[test]
195    fn test_external_library_error() {
196        let err = AstroError::external_library_error("telescope_driver", -2, "mount error");
197        assert!(err.to_string().contains("mount error"));
198        assert!(err.to_string().contains("telescope_driver"));
199        assert!(err.to_string().contains("status -2"));
200    }
201
202    #[test]
203    fn test_data_error() {
204        let err = AstroError::data_error("IERS Bulletin A", "download", "network timeout");
205        assert!(err
206            .to_string()
207            .contains("Data error (IERS Bulletin A - download)"));
208    }
209
210    #[test]
211    fn test_calculation_error() {
212        let err = AstroError::calculation_error("orbit propagation", "insufficient data");
213        assert!(err
214            .to_string()
215            .contains("Calculation error in orbit propagation"));
216    }
217
218    #[test]
219    fn test_recoverable_errors() {
220        assert!(AstroError::data_error("catalog", "download", "timeout").is_recoverable());
221        assert!(!AstroError::invalid_date(2000, 13, 1, "bad month").is_recoverable());
222    }
223
224    #[test]
225    fn test_send_sync() {
226        fn _assert_send<T: Send>() {}
227        fn _assert_sync<T: Sync>() {}
228        _assert_send::<AstroError>();
229        _assert_sync::<AstroError>();
230    }
231
232    #[test]
233    fn test_non_recoverable_errors() {
234        let math_err =
235            AstroError::math_error("calculation", MathErrorKind::Overflow, "value too large");
236        assert!(!math_err.is_recoverable());
237
238        let lib_err = AstroError::external_library_error("telescope_driver", -1, "mount error");
239        assert!(!lib_err.is_recoverable());
240
241        let calc_err = AstroError::calculation_error("orbit_propagation", "insufficient data");
242        assert!(!calc_err.is_recoverable());
243    }
244}