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 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 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 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 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#[derive(Debug, Clone)]
450pub struct ErrorContext {
451 pub category: &'static str,
453 pub details: Vec<(String, String)>,
455}
456
457impl ErrorContext {
458 pub fn new(category: &'static str) -> Self {
460 Self {
461 category,
462 details: Vec::new(),
463 }
464 }
465
466 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}