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    /// Expression nesting depth exceeded
139    ///
140    /// Raised by the raster-algebra expression front-ends when an input
141    /// expression nests more deeply than [`crate::MAX_EXPRESSION_DEPTH`].
142    /// The limit exists so that untrusted expressions cannot exhaust the
143    /// thread stack via unbounded recursive descent.
144    #[cfg_attr(
145        feature = "std",
146        error(
147            "Expression nesting too deep in {context}: depth {depth} exceeds the maximum of {max}"
148        )
149    )]
150    NestingTooDeep {
151        /// Where the limit was hit (e.g. `"dsl"`, `"expression"`)
152        context: &'static str,
153        /// Observed (or conservatively estimated) nesting depth
154        depth: usize,
155        /// Maximum permitted nesting depth
156        max: usize,
157    },
158}
159
160#[cfg(not(feature = "std"))]
161impl fmt::Display for AlgorithmError {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::Core(e) => write!(f, "Core error: {e}"),
165            Self::InvalidDimensions {
166                message,
167                actual,
168                expected,
169            } => write!(
170                f,
171                "Invalid dimensions: {message} (got {actual}, expected {expected})"
172            ),
173            Self::EmptyInput { operation } => write!(f, "Empty input: {operation}"),
174            Self::InvalidInput(message) => write!(f, "Invalid input: {message}"),
175            Self::InvalidParameter { parameter, message } => {
176                write!(f, "Invalid parameter '{parameter}': {message}")
177            }
178            Self::InvalidGeometry(message) => write!(f, "Invalid geometry: {message}"),
179            Self::IncompatibleTypes {
180                source_type,
181                target_type,
182            } => write!(f, "Incompatible types: {source_type} and {target_type}"),
183            Self::InsufficientData { operation, message } => {
184                write!(f, "Insufficient data for {operation}: {message}")
185            }
186            Self::NumericalError { operation, message } => {
187                write!(f, "Numerical error in {operation}: {message}")
188            }
189            Self::ComputationError(message) => {
190                write!(f, "Computation error: {message}")
191            }
192            Self::GeometryError { message } => write!(f, "Geometry error: {message}"),
193            Self::UnsupportedOperation { operation } => {
194                write!(f, "Unsupported operation: {operation}")
195            }
196            Self::AllocationFailed { message } => {
197                write!(f, "Memory allocation failed: {message}")
198            }
199            Self::SimdNotAvailable => write!(f, "SIMD instructions not available"),
200            Self::PathNotFound(message) => write!(f, "Path not found: {message}"),
201            Self::NestingTooDeep {
202                context,
203                depth,
204                max,
205            } => write!(
206                f,
207                "Expression nesting too deep in {context}: depth {depth} exceeds the maximum of {max}"
208            ),
209        }
210    }
211}
212
213impl AlgorithmError {
214    /// Get the error code for this algorithm error
215    ///
216    /// Error codes are stable across versions and can be used for documentation
217    /// and error handling.
218    pub fn code(&self) -> &'static str {
219        match self {
220            Self::Core(_) => "A001",
221            Self::InvalidDimensions { .. } => "A002",
222            Self::EmptyInput { .. } => "A003",
223            Self::InvalidInput(_) => "A004",
224            Self::InvalidParameter { .. } => "A005",
225            Self::InvalidGeometry(_) => "A006",
226            Self::IncompatibleTypes { .. } => "A007",
227            Self::InsufficientData { .. } => "A008",
228            Self::NumericalError { .. } => "A009",
229            Self::ComputationError(_) => "A010",
230            Self::GeometryError { .. } => "A011",
231            Self::UnsupportedOperation { .. } => "A012",
232            Self::AllocationFailed { .. } => "A013",
233            Self::SimdNotAvailable => "A014",
234            Self::PathNotFound(_) => "A015",
235            Self::NestingTooDeep { .. } => "A016",
236        }
237    }
238
239    /// Get a helpful suggestion for fixing this algorithm error
240    ///
241    /// Returns a human-readable suggestion including parameter constraints and valid ranges.
242    pub fn suggestion(&self) -> Option<&'static str> {
243        match self {
244            Self::Core(_) => Some("Check the underlying error for details"),
245            Self::InvalidDimensions { message, .. } => {
246                // Provide specific suggestions based on common dimension errors
247                if message.contains("window") {
248                    Some(
249                        "Window size must be odd. Try adjusting to the nearest odd number (e.g., 3, 5, 7)",
250                    )
251                } else if message.contains("kernel") {
252                    Some("Kernel size must be odd and positive. Common values are 3, 5, 7, or 9")
253                } else {
254                    Some("Check that array dimensions match the expected shape")
255                }
256            }
257            Self::EmptyInput { .. } => Some("Provide at least one data point or feature"),
258            Self::InvalidInput(_) => Some("Verify input data format and values are correct"),
259            Self::InvalidParameter { parameter, message } => {
260                // Provide specific suggestions based on parameter name
261                if parameter.contains("window") || parameter.contains("kernel") {
262                    Some("Window/kernel size must be odd and positive (e.g., 3, 5, 7)")
263                } else if parameter.contains("threshold") {
264                    Some("Threshold values are typically between 0.0 and 1.0")
265                } else if parameter.contains("radius") {
266                    Some("Radius must be positive and reasonable for your data resolution")
267                } else if parameter.contains("iterations") {
268                    Some("Number of iterations must be positive (typically 1-1000)")
269                } else if message.contains("odd") {
270                    Some("Value must be odd. Try the next odd number (current±1)")
271                } else if message.contains("positive") {
272                    Some("Value must be greater than zero")
273                } else if message.contains("range") {
274                    Some("Value must be within the specified range")
275                } else {
276                    Some("Check parameter documentation for valid values and constraints")
277                }
278            }
279            Self::InvalidGeometry(_) => Some("Verify geometry is valid and not self-intersecting"),
280            Self::IncompatibleTypes { .. } => {
281                Some("Convert data to compatible types before processing")
282            }
283            Self::InsufficientData { .. } => Some("Provide more data points for reliable results"),
284            Self::NumericalError { .. } => {
285                Some("Check for division by zero, overflow, or invalid mathematical operations")
286            }
287            Self::ComputationError(_) => Some("Verify input data is within acceptable ranges"),
288            Self::GeometryError { .. } => Some("Check geometry validity and topology"),
289            Self::UnsupportedOperation { .. } => {
290                Some("Use a different algorithm or enable required features")
291            }
292            Self::AllocationFailed { .. } => Some("Reduce data size or increase available memory"),
293            Self::SimdNotAvailable => Some(
294                "SIMD operations are not supported on this CPU. The algorithm will use scalar fallback",
295            ),
296            Self::PathNotFound(_) => Some("Verify the path exists and is accessible"),
297            Self::NestingTooDeep { .. } => Some(
298                "Reduce the nesting of the expression, or split it into intermediate `let` bindings",
299            ),
300        }
301    }
302
303    /// Get additional context about this algorithm error
304    ///
305    /// Returns structured context information including parameter names and values.
306    pub fn context(&self) -> ErrorContext {
307        match self {
308            Self::Core(e) => ErrorContext::new("core_error").with_detail("error", e.to_string()),
309            Self::InvalidDimensions {
310                message,
311                actual,
312                expected,
313            } => ErrorContext::new("invalid_dimensions")
314                .with_detail("message", message.to_string())
315                .with_detail("actual", actual.to_string())
316                .with_detail("expected", expected.to_string()),
317            Self::EmptyInput { operation } => {
318                ErrorContext::new("empty_input").with_detail("operation", operation.to_string())
319            }
320            Self::InvalidInput(msg) => {
321                ErrorContext::new("invalid_input").with_detail("message", msg.clone())
322            }
323            Self::InvalidParameter { parameter, message } => ErrorContext::new("invalid_parameter")
324                .with_detail("parameter", parameter.to_string())
325                .with_detail("message", message.clone()),
326            Self::InvalidGeometry(msg) => {
327                ErrorContext::new("invalid_geometry").with_detail("message", msg.clone())
328            }
329            Self::IncompatibleTypes {
330                source_type,
331                target_type,
332            } => ErrorContext::new("incompatible_types")
333                .with_detail("source_type", source_type.to_string())
334                .with_detail("target_type", target_type.to_string()),
335            Self::InsufficientData { operation, message } => ErrorContext::new("insufficient_data")
336                .with_detail("operation", operation.to_string())
337                .with_detail("message", message.clone()),
338            Self::NumericalError { operation, message } => ErrorContext::new("numerical_error")
339                .with_detail("operation", operation.to_string())
340                .with_detail("message", message.clone()),
341            Self::ComputationError(msg) => {
342                ErrorContext::new("computation_error").with_detail("message", msg.clone())
343            }
344            Self::GeometryError { message } => {
345                ErrorContext::new("geometry_error").with_detail("message", message.clone())
346            }
347            Self::UnsupportedOperation { operation } => ErrorContext::new("unsupported_operation")
348                .with_detail("operation", operation.clone()),
349            Self::AllocationFailed { message } => {
350                ErrorContext::new("allocation_failed").with_detail("message", message.clone())
351            }
352            Self::SimdNotAvailable => ErrorContext::new("simd_not_available"),
353            Self::PathNotFound(path) => {
354                ErrorContext::new("path_not_found").with_detail("path", path.clone())
355            }
356            Self::NestingTooDeep {
357                context,
358                depth,
359                max,
360            } => ErrorContext::new("nesting_too_deep")
361                .with_detail("context", context.to_string())
362                .with_detail("depth", depth.to_string())
363                .with_detail("max", max.to_string()),
364        }
365    }
366}
367
368/// Additional context information for algorithm errors
369#[derive(Debug, Clone)]
370pub struct ErrorContext {
371    /// Error category for grouping similar errors
372    pub category: &'static str,
373    /// Additional details about the error
374    pub details: Vec<(String, String)>,
375}
376
377impl ErrorContext {
378    /// Create a new error context
379    pub fn new(category: &'static str) -> Self {
380        Self {
381            category,
382            details: Vec::new(),
383        }
384    }
385
386    /// Add a detail to the context
387    pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
388        self.details.push((key.into(), value.into()));
389        self
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn test_error_display() {
399        let err = AlgorithmError::InvalidParameter {
400            parameter: "window_size",
401            message: "must be positive".to_string(),
402        };
403        let s = format!("{err}");
404        assert!(s.contains("window_size"));
405        assert!(s.contains("must be positive"));
406    }
407
408    #[test]
409    fn test_error_from_core() {
410        let core_err = OxiGdalError::OutOfBounds {
411            message: "test".to_string(),
412        };
413        let _alg_err: AlgorithmError = core_err.into();
414    }
415
416    #[test]
417    fn test_invalid_input() {
418        let err = AlgorithmError::InvalidInput("test input".to_string());
419        let s = format!("{err}");
420        assert!(s.contains("Invalid input"));
421        assert!(s.contains("test input"));
422    }
423
424    #[test]
425    fn test_invalid_geometry() {
426        let err = AlgorithmError::InvalidGeometry("test geometry".to_string());
427        let s = format!("{err}");
428        assert!(s.contains("Invalid geometry"));
429        assert!(s.contains("test geometry"));
430    }
431
432    #[test]
433    fn test_computation_error() {
434        let err = AlgorithmError::ComputationError("test error".to_string());
435        let s = format!("{err}");
436        assert!(s.contains("Computation error"));
437        assert!(s.contains("test error"));
438    }
439
440    #[test]
441    fn test_error_codes() {
442        let err = AlgorithmError::InvalidParameter {
443            parameter: "window_size",
444            message: "must be odd".to_string(),
445        };
446        assert_eq!(err.code(), "A005");
447
448        let err = AlgorithmError::InvalidDimensions {
449            message: "mismatched",
450            actual: 4,
451            expected: 5,
452        };
453        assert_eq!(err.code(), "A002");
454
455        let err = AlgorithmError::SimdNotAvailable;
456        assert_eq!(err.code(), "A014");
457    }
458
459    #[test]
460    fn test_error_suggestions() {
461        let err = AlgorithmError::InvalidParameter {
462            parameter: "window_size",
463            message: "must be odd".to_string(),
464        };
465        assert!(err.suggestion().is_some());
466        assert!(err.suggestion().is_some_and(|s| s.contains("odd")));
467
468        let err = AlgorithmError::InvalidParameter {
469            parameter: "kernel_size",
470            message: "invalid".to_string(),
471        };
472        assert!(err.suggestion().is_some());
473        assert!(err.suggestion().is_some_and(|s| s.contains("kernel")));
474
475        let err = AlgorithmError::InvalidDimensions {
476            message: "window size",
477            actual: 4,
478            expected: 3,
479        };
480        assert!(err.suggestion().is_some());
481        assert!(
482            err.suggestion()
483                .is_some_and(|s| s.contains("Window size must be odd"))
484        );
485    }
486
487    #[test]
488    fn test_error_context() {
489        let err = AlgorithmError::InvalidParameter {
490            parameter: "window_size",
491            message: "must be odd, got 4. Try 3 or 5".to_string(),
492        };
493        let ctx = err.context();
494        assert_eq!(ctx.category, "invalid_parameter");
495        assert!(
496            ctx.details
497                .iter()
498                .any(|(k, v)| k == "parameter" && v == "window_size")
499        );
500        assert!(ctx.details.iter().any(|(k, _)| k == "message"));
501
502        let err = AlgorithmError::InvalidDimensions {
503            message: "array size mismatch",
504            actual: 100,
505            expected: 200,
506        };
507        let ctx = err.context();
508        assert_eq!(ctx.category, "invalid_dimensions");
509        assert!(ctx.details.iter().any(|(k, v)| k == "actual" && v == "100"));
510        assert!(
511            ctx.details
512                .iter()
513                .any(|(k, v)| k == "expected" && v == "200")
514        );
515    }
516
517    #[test]
518    fn test_parameter_suggestion_specificity() {
519        // Test window parameter
520        let err = AlgorithmError::InvalidParameter {
521            parameter: "window_size",
522            message: "test".to_string(),
523        };
524        let suggestion = err.suggestion();
525        assert!(suggestion.is_some_and(|s| s.contains("Window") || s.contains("kernel")));
526
527        // Test threshold parameter
528        let err = AlgorithmError::InvalidParameter {
529            parameter: "threshold",
530            message: "test".to_string(),
531        };
532        let suggestion = err.suggestion();
533        assert!(suggestion.is_some_and(|s| s.contains("0.0") && s.contains("1.0")));
534    }
535}