Skip to main content

oxigdal_netcdf/
error.rs

1//! Error types for NetCDF operations.
2//!
3//! This module provides comprehensive error handling for NetCDF file operations,
4//! including I/O errors, format errors, dimension errors, variable errors, and
5//! attribute errors.
6//!
7//! # Error Codes
8//!
9//! Each error variant has an associated error code (e.g., N001, N002) for easier
10//! debugging and documentation. Error codes are stable across versions.
11//!
12//! # Helper Methods
13//!
14//! All error types provide:
15//! - `code()` - Returns the error code
16//! - `suggestion()` - Returns helpful hints for fixing the error
17//! - `context()` - Returns additional context about the error (variable/dimension names, etc.)
18
19use core::fmt;
20
21#[cfg(feature = "std")]
22use std::error::Error as StdError;
23
24use oxigdal_core::error::OxiGdalError;
25
26/// Result type for NetCDF operations.
27pub type Result<T> = core::result::Result<T, NetCdfError>;
28
29/// NetCDF-specific error types.
30#[derive(Debug)]
31pub enum NetCdfError {
32    /// I/O error occurred
33    Io(String),
34
35    /// Invalid NetCDF format
36    InvalidFormat(String),
37
38    /// Version not supported
39    UnsupportedVersion { version: u8, message: String },
40
41    /// Dimension error
42    DimensionError(String),
43
44    /// Dimension not found
45    DimensionNotFound { name: String },
46
47    /// Variable error
48    VariableError(String),
49
50    /// Variable not found
51    VariableNotFound { name: String },
52
53    /// Attribute error
54    AttributeError(String),
55
56    /// Attribute not found
57    AttributeNotFound { name: String },
58
59    /// Data type mismatch
60    DataTypeMismatch { expected: String, found: String },
61
62    /// Invalid shape or dimensions
63    InvalidShape { message: String },
64
65    /// Unlimited dimension error
66    UnlimitedDimensionError(String),
67
68    /// Index out of bounds
69    IndexOutOfBounds {
70        index: usize,
71        length: usize,
72        dimension: String,
73    },
74
75    /// String encoding error
76    StringEncodingError(String),
77
78    /// Feature not enabled
79    FeatureNotEnabled { feature: String, message: String },
80
81    /// NetCDF-4 not available (requires feature flag)
82    NetCdf4NotAvailable,
83
84    /// Compression not supported
85    CompressionNotSupported { compression: String },
86
87    /// Invalid compression parameters
88    InvalidCompressionParams(String),
89
90    /// CF conventions error
91    CfConventionsError(String),
92
93    /// Coordinate variable error
94    CoordinateError(String),
95
96    /// File already exists
97    FileAlreadyExists { path: String },
98
99    /// File not found
100    FileNotFound { path: String },
101
102    /// Permission denied
103    PermissionDenied { path: String },
104
105    /// Invalid file mode
106    InvalidFileMode { mode: String },
107
108    /// NetCDF library error (for netcdf4 feature)
109    #[cfg(feature = "netcdf4")]
110    NetCdfLibError(String),
111
112    /// Generic error
113    Other(String),
114
115    /// Error from oxigdal-core
116    Core(OxiGdalError),
117}
118
119impl fmt::Display for NetCdfError {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        match self {
122            Self::Io(msg) => write!(f, "I/O error: {msg}"),
123            Self::InvalidFormat(msg) => write!(f, "Invalid NetCDF format: {msg}"),
124            Self::UnsupportedVersion { version, message } => {
125                write!(f, "Unsupported NetCDF version {version}: {message}")
126            }
127            Self::DimensionError(msg) => write!(f, "Dimension error: {msg}"),
128            Self::DimensionNotFound { name } => write!(f, "Dimension not found: {name}"),
129            Self::VariableError(msg) => write!(f, "Variable error: {msg}"),
130            Self::VariableNotFound { name } => write!(f, "Variable not found: {name}"),
131            Self::AttributeError(msg) => write!(f, "Attribute error: {msg}"),
132            Self::AttributeNotFound { name } => write!(f, "Attribute not found: {name}"),
133            Self::DataTypeMismatch { expected, found } => {
134                write!(f, "Data type mismatch: expected {expected}, found {found}")
135            }
136            Self::InvalidShape { message } => write!(f, "Invalid shape: {message}"),
137            Self::UnlimitedDimensionError(msg) => {
138                write!(f, "Unlimited dimension error: {msg}")
139            }
140            Self::IndexOutOfBounds {
141                index,
142                length,
143                dimension,
144            } => {
145                write!(
146                    f,
147                    "Index {index} out of bounds for dimension '{dimension}' with length {length}"
148                )
149            }
150            Self::StringEncodingError(msg) => write!(f, "String encoding error: {msg}"),
151            Self::FeatureNotEnabled { feature, message } => {
152                write!(f, "Feature '{feature}' not enabled: {message}")
153            }
154            Self::NetCdf4NotAvailable => {
155                write!(
156                    f,
157                    "NetCDF-4 support not available. Enable 'netcdf4' feature to use HDF5-based NetCDF-4 files. \
158                     Note: This requires C dependencies (libnetcdf, libhdf5) and is not Pure Rust."
159                )
160            }
161            Self::CompressionNotSupported { compression } => {
162                write!(f, "Compression not supported: {compression}")
163            }
164            Self::InvalidCompressionParams(msg) => {
165                write!(f, "Invalid compression parameters: {msg}")
166            }
167            Self::CfConventionsError(msg) => write!(f, "CF conventions error: {msg}"),
168            Self::CoordinateError(msg) => write!(f, "Coordinate error: {msg}"),
169            Self::FileAlreadyExists { path } => write!(f, "File already exists: {path}"),
170            Self::FileNotFound { path } => write!(f, "File not found: {path}"),
171            Self::PermissionDenied { path } => write!(f, "Permission denied: {path}"),
172            Self::InvalidFileMode { mode } => write!(f, "Invalid file mode: {mode}"),
173            #[cfg(feature = "netcdf4")]
174            Self::NetCdfLibError(msg) => write!(f, "NetCDF library error: {msg}"),
175            Self::Other(msg) => write!(f, "{msg}"),
176            Self::Core(err) => write!(f, "Core error: {err}"),
177        }
178    }
179}
180
181#[cfg(feature = "std")]
182impl StdError for NetCdfError {
183    fn source(&self) -> Option<&(dyn StdError + 'static)> {
184        match self {
185            Self::Core(err) => Some(err),
186            _ => None,
187        }
188    }
189}
190
191impl From<OxiGdalError> for NetCdfError {
192    fn from(err: OxiGdalError) -> Self {
193        Self::Core(err)
194    }
195}
196
197#[cfg(feature = "std")]
198impl From<std::io::Error> for NetCdfError {
199    fn from(err: std::io::Error) -> Self {
200        use std::io::ErrorKind;
201        match err.kind() {
202            ErrorKind::NotFound => Self::Io(format!("File not found: {err}")),
203            ErrorKind::PermissionDenied => Self::Io(format!("Permission denied: {err}")),
204            ErrorKind::AlreadyExists => Self::Io(format!("File already exists: {err}")),
205            _ => Self::Io(err.to_string()),
206        }
207    }
208}
209
210#[cfg(feature = "std")]
211impl From<std::string::FromUtf8Error> for NetCdfError {
212    fn from(err: std::string::FromUtf8Error) -> Self {
213        Self::StringEncodingError(err.to_string())
214    }
215}
216
217impl From<core::str::Utf8Error> for NetCdfError {
218    fn from(err: core::str::Utf8Error) -> Self {
219        Self::StringEncodingError(format!("UTF-8 error: {err}"))
220    }
221}
222
223#[cfg(feature = "netcdf4")]
224impl From<netcdf::error::Error> for NetCdfError {
225    fn from(err: netcdf::error::Error) -> Self {
226        Self::NetCdfLibError(err.to_string())
227    }
228}
229
230impl From<serde_json::Error> for NetCdfError {
231    fn from(err: serde_json::Error) -> Self {
232        Self::Other(format!("JSON error: {err}"))
233    }
234}
235
236#[cfg(feature = "netcdf3")]
237impl From<netcdf3::InvalidDataSet> for NetCdfError {
238    fn from(err: netcdf3::InvalidDataSet) -> Self {
239        Self::Other(format!("Invalid DataSet: {err}"))
240    }
241}
242
243#[cfg(feature = "netcdf3")]
244impl From<netcdf3::WriteError> for NetCdfError {
245    fn from(err: netcdf3::WriteError) -> Self {
246        Self::Io(format!("Write error: {err:?}"))
247    }
248}
249
250#[cfg(feature = "netcdf3")]
251impl From<netcdf3::ReadError> for NetCdfError {
252    fn from(err: netcdf3::ReadError) -> Self {
253        Self::Io(format!("Read error: {err:?}"))
254    }
255}
256
257impl NetCdfError {
258    /// Get the error code for this NetCDF error
259    ///
260    /// Error codes are stable across versions and can be used for documentation
261    /// and error handling.
262    pub fn code(&self) -> &'static str {
263        match self {
264            Self::Io(_) => "N001",
265            Self::InvalidFormat(_) => "N002",
266            Self::UnsupportedVersion { .. } => "N003",
267            Self::DimensionError(_) => "N004",
268            Self::DimensionNotFound { .. } => "N005",
269            Self::VariableError(_) => "N006",
270            Self::VariableNotFound { .. } => "N007",
271            Self::AttributeError(_) => "N008",
272            Self::AttributeNotFound { .. } => "N009",
273            Self::DataTypeMismatch { .. } => "N010",
274            Self::InvalidShape { .. } => "N011",
275            Self::UnlimitedDimensionError(_) => "N012",
276            Self::IndexOutOfBounds { .. } => "N013",
277            Self::StringEncodingError(_) => "N014",
278            Self::FeatureNotEnabled { .. } => "N015",
279            Self::NetCdf4NotAvailable => "N016",
280            Self::CompressionNotSupported { .. } => "N017",
281            Self::InvalidCompressionParams(_) => "N018",
282            Self::CfConventionsError(_) => "N019",
283            Self::CoordinateError(_) => "N020",
284            Self::FileAlreadyExists { .. } => "N021",
285            Self::FileNotFound { .. } => "N022",
286            Self::PermissionDenied { .. } => "N023",
287            Self::InvalidFileMode { .. } => "N024",
288            #[cfg(feature = "netcdf4")]
289            Self::NetCdfLibError(_) => "N025",
290            Self::Other(_) => "N099",
291            Self::Core(_) => "N100",
292        }
293    }
294
295    /// Get a helpful suggestion for fixing this NetCDF error
296    ///
297    /// Returns a human-readable suggestion that can help users resolve the error.
298    pub fn suggestion(&self) -> Option<&'static str> {
299        match self {
300            Self::Io(_) => Some("Check file permissions and network connectivity"),
301            Self::InvalidFormat(_) => {
302                Some("Verify the file is a valid NetCDF file. Try using ncdump")
303            }
304            Self::UnsupportedVersion { .. } => {
305                Some("This NetCDF version is not supported. Try NetCDF-3 Classic format")
306            }
307            Self::DimensionError(_) => Some("Check dimension definitions and sizes"),
308            Self::DimensionNotFound { .. } => Some("Use ncdump -h to list available dimensions"),
309            Self::VariableError(_) => Some("Check variable definitions and data types"),
310            Self::VariableNotFound { .. } => Some("Use ncdump -h to list available variables"),
311            Self::AttributeError(_) => Some("Check attribute name and type"),
312            Self::AttributeNotFound { .. } => Some("Use ncdump -h to list available attributes"),
313            Self::DataTypeMismatch { .. } => {
314                Some("Ensure the data type matches the variable definition")
315            }
316            Self::InvalidShape { .. } => {
317                Some("Verify the data dimensions match the variable shape")
318            }
319            Self::UnlimitedDimensionError(_) => Some("Check unlimited dimension is defined first"),
320            Self::IndexOutOfBounds { .. } => Some("Verify indices are within dimension bounds"),
321            Self::StringEncodingError(_) => Some("Ensure string data is valid UTF-8"),
322            Self::FeatureNotEnabled { .. } => {
323                Some("Enable the required feature flag in Cargo.toml")
324            }
325            Self::NetCdf4NotAvailable => {
326                Some("Enable the 'netcdf4' feature for HDF5-based NetCDF-4 support")
327            }
328            Self::CompressionNotSupported { .. } => {
329                Some("Use a supported compression algorithm or disable compression")
330            }
331            Self::InvalidCompressionParams(_) => Some("Check compression level is between 0-9"),
332            Self::CfConventionsError(_) => {
333                Some("Ensure the file follows CF conventions. See https://cfconventions.org")
334            }
335            Self::CoordinateError(_) => Some("Check coordinate variable definitions and values"),
336            Self::FileAlreadyExists { .. } => {
337                Some("Choose a different filename or delete the existing file")
338            }
339            Self::FileNotFound { .. } => {
340                Some("Verify the file path is correct and the file exists")
341            }
342            Self::PermissionDenied { .. } => {
343                Some("Check file permissions or run with appropriate privileges")
344            }
345            Self::InvalidFileMode { .. } => {
346                Some("Use a valid file mode: 'r' (read), 'w' (write), or 'a' (append)")
347            }
348            #[cfg(feature = "netcdf4")]
349            Self::NetCdfLibError(_) => Some("Check the NetCDF-C library is properly installed"),
350            Self::Other(_) => Some("Check the error message for details"),
351            Self::Core(_) => Some("Check the underlying error message for details"),
352        }
353    }
354
355    /// Get additional context about this NetCDF error
356    ///
357    /// Returns structured context information including variable/dimension names.
358    pub fn context(&self) -> ErrorContext {
359        match self {
360            Self::Io(msg) => ErrorContext::new("io_error").with_detail("message", msg.clone()),
361            Self::InvalidFormat(msg) => {
362                ErrorContext::new("invalid_format").with_detail("message", msg.clone())
363            }
364            Self::UnsupportedVersion { version, message } => {
365                ErrorContext::new("unsupported_version")
366                    .with_detail("version", version.to_string())
367                    .with_detail("message", message.clone())
368            }
369            Self::DimensionError(msg) => {
370                ErrorContext::new("dimension_error").with_detail("message", msg.clone())
371            }
372            Self::DimensionNotFound { name } => {
373                ErrorContext::new("dimension_not_found").with_detail("dimension", name.clone())
374            }
375            Self::VariableError(msg) => {
376                ErrorContext::new("variable_error").with_detail("message", msg.clone())
377            }
378            Self::VariableNotFound { name } => {
379                ErrorContext::new("variable_not_found").with_detail("variable", name.clone())
380            }
381            Self::AttributeError(msg) => {
382                ErrorContext::new("attribute_error").with_detail("message", msg.clone())
383            }
384            Self::AttributeNotFound { name } => {
385                ErrorContext::new("attribute_not_found").with_detail("attribute", name.clone())
386            }
387            Self::DataTypeMismatch { expected, found } => ErrorContext::new("data_type_mismatch")
388                .with_detail("expected", expected.clone())
389                .with_detail("found", found.clone()),
390            Self::InvalidShape { message } => {
391                ErrorContext::new("invalid_shape").with_detail("message", message.clone())
392            }
393            Self::UnlimitedDimensionError(msg) => {
394                ErrorContext::new("unlimited_dimension_error").with_detail("message", msg.clone())
395            }
396            Self::IndexOutOfBounds {
397                index,
398                length,
399                dimension,
400            } => ErrorContext::new("index_out_of_bounds")
401                .with_detail("index", index.to_string())
402                .with_detail("length", length.to_string())
403                .with_detail("dimension", dimension.clone()),
404            Self::StringEncodingError(msg) => {
405                ErrorContext::new("string_encoding_error").with_detail("message", msg.clone())
406            }
407            Self::FeatureNotEnabled { feature, message } => {
408                ErrorContext::new("feature_not_enabled")
409                    .with_detail("feature", feature.clone())
410                    .with_detail("message", message.clone())
411            }
412            Self::NetCdf4NotAvailable => ErrorContext::new("netcdf4_not_available"),
413            Self::CompressionNotSupported { compression } => {
414                ErrorContext::new("compression_not_supported")
415                    .with_detail("compression", compression.clone())
416            }
417            Self::InvalidCompressionParams(msg) => {
418                ErrorContext::new("invalid_compression_params").with_detail("message", msg.clone())
419            }
420            Self::CfConventionsError(msg) => {
421                ErrorContext::new("cf_conventions_error").with_detail("message", msg.clone())
422            }
423            Self::CoordinateError(msg) => {
424                ErrorContext::new("coordinate_error").with_detail("message", msg.clone())
425            }
426            Self::FileAlreadyExists { path } => {
427                ErrorContext::new("file_already_exists").with_detail("path", path.clone())
428            }
429            Self::FileNotFound { path } => {
430                ErrorContext::new("file_not_found").with_detail("path", path.clone())
431            }
432            Self::PermissionDenied { path } => {
433                ErrorContext::new("permission_denied").with_detail("path", path.clone())
434            }
435            Self::InvalidFileMode { mode } => {
436                ErrorContext::new("invalid_file_mode").with_detail("mode", mode.clone())
437            }
438            #[cfg(feature = "netcdf4")]
439            Self::NetCdfLibError(msg) => {
440                ErrorContext::new("netcdf_lib_error").with_detail("message", msg.clone())
441            }
442            Self::Other(msg) => ErrorContext::new("other").with_detail("message", msg.clone()),
443            Self::Core(e) => ErrorContext::new("core_error").with_detail("error", e.to_string()),
444        }
445    }
446}
447
448/// Additional context information for NetCDF errors
449#[derive(Debug, Clone)]
450pub struct ErrorContext {
451    /// Error category for grouping similar errors
452    pub category: &'static str,
453    /// Additional details about the error (variable/dimension names, etc.)
454    pub details: Vec<(String, String)>,
455}
456
457impl ErrorContext {
458    /// Create a new error context
459    pub fn new(category: &'static str) -> Self {
460        Self {
461            category,
462            details: Vec::new(),
463        }
464    }
465
466    /// Add a detail to the context
467    pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
468        self.details.push((key.into(), value.into()));
469        self
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_error_display() {
479        let err = NetCdfError::DimensionNotFound {
480            name: "time".to_string(),
481        };
482        assert_eq!(err.to_string(), "Dimension not found: time");
483
484        let err = NetCdfError::DataTypeMismatch {
485            expected: "f32".to_string(),
486            found: "f64".to_string(),
487        };
488        assert_eq!(
489            err.to_string(),
490            "Data type mismatch: expected f32, found f64"
491        );
492
493        let err = NetCdfError::IndexOutOfBounds {
494            index: 10,
495            length: 5,
496            dimension: "time".to_string(),
497        };
498        assert_eq!(
499            err.to_string(),
500            "Index 10 out of bounds for dimension 'time' with length 5"
501        );
502    }
503
504    #[test]
505    fn test_netcdf4_not_available() {
506        let err = NetCdfError::NetCdf4NotAvailable;
507        let msg = err.to_string();
508        assert!(msg.contains("NetCDF-4"));
509        assert!(msg.contains("netcdf4"));
510        assert!(msg.contains("Pure Rust"));
511    }
512
513    #[test]
514    fn test_error_codes() {
515        let err = NetCdfError::VariableNotFound {
516            name: "temperature".to_string(),
517        };
518        assert_eq!(err.code(), "N007");
519
520        let err = NetCdfError::DimensionNotFound {
521            name: "time".to_string(),
522        };
523        assert_eq!(err.code(), "N005");
524
525        let err = NetCdfError::AttributeNotFound {
526            name: "units".to_string(),
527        };
528        assert_eq!(err.code(), "N009");
529    }
530
531    #[test]
532    fn test_error_suggestions() {
533        let err = NetCdfError::VariableNotFound {
534            name: "temperature".to_string(),
535        };
536        assert!(err.suggestion().is_some());
537        assert!(err.suggestion().is_some_and(|s| s.contains("ncdump")));
538
539        let err = NetCdfError::DimensionNotFound {
540            name: "time".to_string(),
541        };
542        assert!(err.suggestion().is_some());
543        assert!(err.suggestion().is_some_and(|s| s.contains("ncdump")));
544    }
545
546    #[test]
547    fn test_error_context() {
548        let err = NetCdfError::VariableNotFound {
549            name: "temperature".to_string(),
550        };
551        let ctx = err.context();
552        assert_eq!(ctx.category, "variable_not_found");
553        assert!(
554            ctx.details
555                .iter()
556                .any(|(k, v)| k == "variable" && v == "temperature")
557        );
558
559        let err = NetCdfError::DimensionNotFound {
560            name: "time".to_string(),
561        };
562        let ctx = err.context();
563        assert_eq!(ctx.category, "dimension_not_found");
564        assert!(
565            ctx.details
566                .iter()
567                .any(|(k, v)| k == "dimension" && v == "time")
568        );
569
570        let err = NetCdfError::IndexOutOfBounds {
571            index: 10,
572            length: 5,
573            dimension: "time".to_string(),
574        };
575        let ctx = err.context();
576        assert_eq!(ctx.category, "index_out_of_bounds");
577        assert!(ctx.details.iter().any(|(k, v)| k == "index" && v == "10"));
578        assert!(
579            ctx.details
580                .iter()
581                .any(|(k, v)| k == "dimension" && v == "time")
582        );
583    }
584}