Skip to main content

oxigdal_algorithms/
error.rs

1//! Error types for OxiGDAL algorithms
2//!
3//! # Error Codes
4//!
5//! Each error variant has an associated error code (e.g., A001, A002) for easier
6//! debugging and documentation. Error codes are stable across versions.
7//!
8//! # Helper Methods
9//!
10//! All error types provide:
11//! - `code()` - Returns the error code
12//! - `suggestion()` - Returns helpful hints including parameter constraints
13//! - `context()` - Returns additional context about the error
14
15#[cfg(not(feature = "std"))]
16use core::fmt;
17
18#[cfg(feature = "std")]
19use thiserror::Error;
20
21use oxigdal_core::OxiGdalError;
22
23/// Result type for algorithm operations
24pub type Result<T> = core::result::Result<T, AlgorithmError>;
25
26/// Algorithm-specific errors
27#[derive(Debug)]
28#[cfg_attr(feature = "std", derive(Error))]
29pub enum AlgorithmError {
30    /// Core OxiGDAL error
31    #[cfg_attr(feature = "std", error("Core error: {0}"))]
32    Core(#[from] OxiGdalError),
33
34    /// Invalid dimensions
35    #[cfg_attr(
36        feature = "std",
37        error("Invalid dimensions: {message} (got {actual}, expected {expected})")
38    )]
39    InvalidDimensions {
40        /// Error message
41        message: &'static str,
42        /// Actual dimension
43        actual: usize,
44        /// Expected dimension
45        expected: usize,
46    },
47
48    /// Empty input
49    #[cfg_attr(feature = "std", error("Empty input: {operation}"))]
50    EmptyInput {
51        /// Operation name
52        operation: &'static str,
53    },
54
55    /// Invalid input
56    #[cfg_attr(feature = "std", error("Invalid input: {0}"))]
57    InvalidInput(String),
58
59    /// Invalid parameter
60    #[cfg_attr(feature = "std", error("Invalid parameter '{parameter}': {message}"))]
61    InvalidParameter {
62        /// Parameter name
63        parameter: &'static str,
64        /// Error message
65        message: String,
66    },
67
68    /// Invalid geometry
69    #[cfg_attr(feature = "std", error("Invalid geometry: {0}"))]
70    InvalidGeometry(String),
71
72    /// Incompatible data types
73    #[cfg_attr(
74        feature = "std",
75        error("Incompatible data types: {source_type} and {target_type}")
76    )]
77    IncompatibleTypes {
78        /// Source data type
79        source_type: &'static str,
80        /// Target data type
81        target_type: &'static str,
82    },
83
84    /// Insufficient data
85    #[cfg_attr(feature = "std", error("Insufficient data for {operation}: {message}"))]
86    InsufficientData {
87        /// Operation name
88        operation: &'static str,
89        /// Error message
90        message: String,
91    },
92
93    /// Numerical error
94    #[cfg_attr(feature = "std", error("Numerical error in {operation}: {message}"))]
95    NumericalError {
96        /// Operation name
97        operation: &'static str,
98        /// Error message
99        message: String,
100    },
101
102    /// Computation error
103    #[cfg_attr(feature = "std", error("Computation error: {0}"))]
104    ComputationError(String),
105
106    /// Geometry error
107    #[cfg_attr(feature = "std", error("Geometry error: {message}"))]
108    GeometryError {
109        /// Error message
110        message: String,
111    },
112
113    /// Unsupported operation
114    #[cfg_attr(feature = "std", error("Unsupported operation: {operation}"))]
115    UnsupportedOperation {
116        /// Operation description
117        operation: String,
118    },
119
120    /// Allocation failed
121    #[cfg_attr(feature = "std", error("Memory allocation failed: {message}"))]
122    AllocationFailed {
123        /// Error message
124        message: String,
125    },
126
127    /// SIMD not available
128    #[cfg_attr(
129        feature = "std",
130        error("SIMD instructions not available on this platform")
131    )]
132    SimdNotAvailable,
133
134    /// Path not found
135    #[cfg_attr(feature = "std", error("Path not found: {0}"))]
136    PathNotFound(String),
137}
138
139#[cfg(not(feature = "std"))]
140impl fmt::Display for AlgorithmError {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match self {
143            Self::Core(e) => write!(f, "Core error: {e}"),
144            Self::InvalidDimensions {
145                message,
146                actual,
147                expected,
148            } => write!(
149                f,
150                "Invalid dimensions: {message} (got {actual}, expected {expected})"
151            ),
152            Self::EmptyInput { operation } => write!(f, "Empty input: {operation}"),
153            Self::InvalidInput(message) => write!(f, "Invalid input: {message}"),
154            Self::InvalidParameter { parameter, message } => {
155                write!(f, "Invalid parameter '{parameter}': {message}")
156            }
157            Self::InvalidGeometry(message) => write!(f, "Invalid geometry: {message}"),
158            Self::IncompatibleTypes {
159                source_type,
160                target_type,
161            } => write!(f, "Incompatible types: {source_type} and {target_type}"),
162            Self::InsufficientData { operation, message } => {
163                write!(f, "Insufficient data for {operation}: {message}")
164            }
165            Self::NumericalError { operation, message } => {
166                write!(f, "Numerical error in {operation}: {message}")
167            }
168            Self::ComputationError(message) => {
169                write!(f, "Computation error: {message}")
170            }
171            Self::GeometryError { message } => write!(f, "Geometry error: {message}"),
172            Self::UnsupportedOperation { operation } => {
173                write!(f, "Unsupported operation: {operation}")
174            }
175            Self::AllocationFailed { message } => {
176                write!(f, "Memory allocation failed: {message}")
177            }
178            Self::SimdNotAvailable => write!(f, "SIMD instructions not available"),
179            Self::PathNotFound(message) => write!(f, "Path not found: {message}"),
180        }
181    }
182}
183
184impl AlgorithmError {
185    /// Get the error code for this algorithm error
186    ///
187    /// Error codes are stable across versions and can be used for documentation
188    /// and error handling.
189    pub fn code(&self) -> &'static str {
190        match self {
191            Self::Core(_) => "A001",
192            Self::InvalidDimensions { .. } => "A002",
193            Self::EmptyInput { .. } => "A003",
194            Self::InvalidInput(_) => "A004",
195            Self::InvalidParameter { .. } => "A005",
196            Self::InvalidGeometry(_) => "A006",
197            Self::IncompatibleTypes { .. } => "A007",
198            Self::InsufficientData { .. } => "A008",
199            Self::NumericalError { .. } => "A009",
200            Self::ComputationError(_) => "A010",
201            Self::GeometryError { .. } => "A011",
202            Self::UnsupportedOperation { .. } => "A012",
203            Self::AllocationFailed { .. } => "A013",
204            Self::SimdNotAvailable => "A014",
205            Self::PathNotFound(_) => "A015",
206        }
207    }
208
209    /// Get a helpful suggestion for fixing this algorithm error
210    ///
211    /// Returns a human-readable suggestion including parameter constraints and valid ranges.
212    pub fn suggestion(&self) -> Option<&'static str> {
213        match self {
214            Self::Core(_) => Some("Check the underlying error for details"),
215            Self::InvalidDimensions { message, .. } => {
216                // Provide specific suggestions based on common dimension errors
217                if message.contains("window") {
218                    Some(
219                        "Window size must be odd. Try adjusting to the nearest odd number (e.g., 3, 5, 7)",
220                    )
221                } else if message.contains("kernel") {
222                    Some("Kernel size must be odd and positive. Common values are 3, 5, 7, or 9")
223                } else {
224                    Some("Check that array dimensions match the expected shape")
225                }
226            }
227            Self::EmptyInput { .. } => Some("Provide at least one data point or feature"),
228            Self::InvalidInput(_) => Some("Verify input data format and values are correct"),
229            Self::InvalidParameter { parameter, message } => {
230                // Provide specific suggestions based on parameter name
231                if parameter.contains("window") || parameter.contains("kernel") {
232                    Some("Window/kernel size must be odd and positive (e.g., 3, 5, 7)")
233                } else if parameter.contains("threshold") {
234                    Some("Threshold values are typically between 0.0 and 1.0")
235                } else if parameter.contains("radius") {
236                    Some("Radius must be positive and reasonable for your data resolution")
237                } else if parameter.contains("iterations") {
238                    Some("Number of iterations must be positive (typically 1-1000)")
239                } else if message.contains("odd") {
240                    Some("Value must be odd. Try the next odd number (current±1)")
241                } else if message.contains("positive") {
242                    Some("Value must be greater than zero")
243                } else if message.contains("range") {
244                    Some("Value must be within the specified range")
245                } else {
246                    Some("Check parameter documentation for valid values and constraints")
247                }
248            }
249            Self::InvalidGeometry(_) => Some("Verify geometry is valid and not self-intersecting"),
250            Self::IncompatibleTypes { .. } => {
251                Some("Convert data to compatible types before processing")
252            }
253            Self::InsufficientData { .. } => Some("Provide more data points for reliable results"),
254            Self::NumericalError { .. } => {
255                Some("Check for division by zero, overflow, or invalid mathematical operations")
256            }
257            Self::ComputationError(_) => Some("Verify input data is within acceptable ranges"),
258            Self::GeometryError { .. } => Some("Check geometry validity and topology"),
259            Self::UnsupportedOperation { .. } => {
260                Some("Use a different algorithm or enable required features")
261            }
262            Self::AllocationFailed { .. } => Some("Reduce data size or increase available memory"),
263            Self::SimdNotAvailable => Some(
264                "SIMD operations are not supported on this CPU. The algorithm will use scalar fallback",
265            ),
266            Self::PathNotFound(_) => Some("Verify the path exists and is accessible"),
267        }
268    }
269
270    /// Get additional context about this algorithm error
271    ///
272    /// Returns structured context information including parameter names and values.
273    pub fn context(&self) -> ErrorContext {
274        match self {
275            Self::Core(e) => ErrorContext::new("core_error").with_detail("error", e.to_string()),
276            Self::InvalidDimensions {
277                message,
278                actual,
279                expected,
280            } => ErrorContext::new("invalid_dimensions")
281                .with_detail("message", message.to_string())
282                .with_detail("actual", actual.to_string())
283                .with_detail("expected", expected.to_string()),
284            Self::EmptyInput { operation } => {
285                ErrorContext::new("empty_input").with_detail("operation", operation.to_string())
286            }
287            Self::InvalidInput(msg) => {
288                ErrorContext::new("invalid_input").with_detail("message", msg.clone())
289            }
290            Self::InvalidParameter { parameter, message } => ErrorContext::new("invalid_parameter")
291                .with_detail("parameter", parameter.to_string())
292                .with_detail("message", message.clone()),
293            Self::InvalidGeometry(msg) => {
294                ErrorContext::new("invalid_geometry").with_detail("message", msg.clone())
295            }
296            Self::IncompatibleTypes {
297                source_type,
298                target_type,
299            } => ErrorContext::new("incompatible_types")
300                .with_detail("source_type", source_type.to_string())
301                .with_detail("target_type", target_type.to_string()),
302            Self::InsufficientData { operation, message } => ErrorContext::new("insufficient_data")
303                .with_detail("operation", operation.to_string())
304                .with_detail("message", message.clone()),
305            Self::NumericalError { operation, message } => ErrorContext::new("numerical_error")
306                .with_detail("operation", operation.to_string())
307                .with_detail("message", message.clone()),
308            Self::ComputationError(msg) => {
309                ErrorContext::new("computation_error").with_detail("message", msg.clone())
310            }
311            Self::GeometryError { message } => {
312                ErrorContext::new("geometry_error").with_detail("message", message.clone())
313            }
314            Self::UnsupportedOperation { operation } => ErrorContext::new("unsupported_operation")
315                .with_detail("operation", operation.clone()),
316            Self::AllocationFailed { message } => {
317                ErrorContext::new("allocation_failed").with_detail("message", message.clone())
318            }
319            Self::SimdNotAvailable => ErrorContext::new("simd_not_available"),
320            Self::PathNotFound(path) => {
321                ErrorContext::new("path_not_found").with_detail("path", path.clone())
322            }
323        }
324    }
325}
326
327/// Additional context information for algorithm errors
328#[derive(Debug, Clone)]
329pub struct ErrorContext {
330    /// Error category for grouping similar errors
331    pub category: &'static str,
332    /// Additional details about the error
333    pub details: Vec<(String, String)>,
334}
335
336impl ErrorContext {
337    /// Create a new error context
338    pub fn new(category: &'static str) -> Self {
339        Self {
340            category,
341            details: Vec::new(),
342        }
343    }
344
345    /// Add a detail to the context
346    pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
347        self.details.push((key.into(), value.into()));
348        self
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_error_display() {
358        let err = AlgorithmError::InvalidParameter {
359            parameter: "window_size",
360            message: "must be positive".to_string(),
361        };
362        let s = format!("{err}");
363        assert!(s.contains("window_size"));
364        assert!(s.contains("must be positive"));
365    }
366
367    #[test]
368    fn test_error_from_core() {
369        let core_err = OxiGdalError::OutOfBounds {
370            message: "test".to_string(),
371        };
372        let _alg_err: AlgorithmError = core_err.into();
373    }
374
375    #[test]
376    fn test_invalid_input() {
377        let err = AlgorithmError::InvalidInput("test input".to_string());
378        let s = format!("{err}");
379        assert!(s.contains("Invalid input"));
380        assert!(s.contains("test input"));
381    }
382
383    #[test]
384    fn test_invalid_geometry() {
385        let err = AlgorithmError::InvalidGeometry("test geometry".to_string());
386        let s = format!("{err}");
387        assert!(s.contains("Invalid geometry"));
388        assert!(s.contains("test geometry"));
389    }
390
391    #[test]
392    fn test_computation_error() {
393        let err = AlgorithmError::ComputationError("test error".to_string());
394        let s = format!("{err}");
395        assert!(s.contains("Computation error"));
396        assert!(s.contains("test error"));
397    }
398
399    #[test]
400    fn test_error_codes() {
401        let err = AlgorithmError::InvalidParameter {
402            parameter: "window_size",
403            message: "must be odd".to_string(),
404        };
405        assert_eq!(err.code(), "A005");
406
407        let err = AlgorithmError::InvalidDimensions {
408            message: "mismatched",
409            actual: 4,
410            expected: 5,
411        };
412        assert_eq!(err.code(), "A002");
413
414        let err = AlgorithmError::SimdNotAvailable;
415        assert_eq!(err.code(), "A014");
416    }
417
418    #[test]
419    fn test_error_suggestions() {
420        let err = AlgorithmError::InvalidParameter {
421            parameter: "window_size",
422            message: "must be odd".to_string(),
423        };
424        assert!(err.suggestion().is_some());
425        assert!(err.suggestion().is_some_and(|s| s.contains("odd")));
426
427        let err = AlgorithmError::InvalidParameter {
428            parameter: "kernel_size",
429            message: "invalid".to_string(),
430        };
431        assert!(err.suggestion().is_some());
432        assert!(err.suggestion().is_some_and(|s| s.contains("kernel")));
433
434        let err = AlgorithmError::InvalidDimensions {
435            message: "window size",
436            actual: 4,
437            expected: 3,
438        };
439        assert!(err.suggestion().is_some());
440        assert!(
441            err.suggestion()
442                .is_some_and(|s| s.contains("Window size must be odd"))
443        );
444    }
445
446    #[test]
447    fn test_error_context() {
448        let err = AlgorithmError::InvalidParameter {
449            parameter: "window_size",
450            message: "must be odd, got 4. Try 3 or 5".to_string(),
451        };
452        let ctx = err.context();
453        assert_eq!(ctx.category, "invalid_parameter");
454        assert!(
455            ctx.details
456                .iter()
457                .any(|(k, v)| k == "parameter" && v == "window_size")
458        );
459        assert!(ctx.details.iter().any(|(k, _)| k == "message"));
460
461        let err = AlgorithmError::InvalidDimensions {
462            message: "array size mismatch",
463            actual: 100,
464            expected: 200,
465        };
466        let ctx = err.context();
467        assert_eq!(ctx.category, "invalid_dimensions");
468        assert!(ctx.details.iter().any(|(k, v)| k == "actual" && v == "100"));
469        assert!(
470            ctx.details
471                .iter()
472                .any(|(k, v)| k == "expected" && v == "200")
473        );
474    }
475
476    #[test]
477    fn test_parameter_suggestion_specificity() {
478        // Test window parameter
479        let err = AlgorithmError::InvalidParameter {
480            parameter: "window_size",
481            message: "test".to_string(),
482        };
483        let suggestion = err.suggestion();
484        assert!(suggestion.is_some_and(|s| s.contains("Window") || s.contains("kernel")));
485
486        // Test threshold parameter
487        let err = AlgorithmError::InvalidParameter {
488            parameter: "threshold",
489            message: "test".to_string(),
490        };
491        let suggestion = err.suggestion();
492        assert!(suggestion.is_some_and(|s| s.contains("0.0") && s.contains("1.0")));
493    }
494}