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