1use serde::{Deserialize, Serialize};
7
8use crate::attribute::{Attribute, AttributeValue, Attributes};
9use crate::dimension::Dimensions;
10use crate::error::{NetCdfError, Result};
11use crate::variable::Variables;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub enum NetCdfVersion {
16 #[default]
18 Classic,
19 Offset64Bit,
21 NetCdf4,
23 NetCdf4Classic,
25}
26
27impl NetCdfVersion {
28 #[must_use]
30 pub const fn is_netcdf4(&self) -> bool {
31 matches!(self, Self::NetCdf4 | Self::NetCdf4Classic)
32 }
33
34 #[must_use]
36 pub const fn is_netcdf3(&self) -> bool {
37 matches!(self, Self::Classic | Self::Offset64Bit)
38 }
39
40 #[must_use]
42 pub const fn version_number(&self) -> u8 {
43 match self {
44 Self::Classic | Self::Offset64Bit => 3,
45 Self::NetCdf4 | Self::NetCdf4Classic => 4,
46 }
47 }
48
49 #[must_use]
51 pub const fn format_name(&self) -> &'static str {
52 match self {
53 Self::Classic => "NetCDF-3 Classic",
54 Self::Offset64Bit => "NetCDF-3 64-bit Offset",
55 Self::NetCdf4 => "NetCDF-4",
56 Self::NetCdf4Classic => "NetCDF-4 Classic",
57 }
58 }
59}
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
65pub struct CfMetadata {
66 pub conventions: Option<String>,
68 pub title: Option<String>,
70 pub institution: Option<String>,
72 pub source: Option<String>,
74 pub history: Option<String>,
76 pub references: Option<String>,
78 pub comment: Option<String>,
80}
81
82impl CfMetadata {
83 #[must_use]
85 pub const fn new() -> Self {
86 Self {
87 conventions: None,
88 title: None,
89 institution: None,
90 source: None,
91 history: None,
92 references: None,
93 comment: None,
94 }
95 }
96
97 #[must_use]
99 pub fn from_attributes(attrs: &Attributes) -> Self {
100 let mut cf = Self::new();
101
102 if let Some(value) = attrs.get_value("Conventions") {
103 if let Ok(s) = value.as_text() {
104 cf.conventions = Some(s.to_string());
105 }
106 }
107
108 if let Some(value) = attrs.get_value("title") {
109 if let Ok(s) = value.as_text() {
110 cf.title = Some(s.to_string());
111 }
112 }
113
114 if let Some(value) = attrs.get_value("institution") {
115 if let Ok(s) = value.as_text() {
116 cf.institution = Some(s.to_string());
117 }
118 }
119
120 if let Some(value) = attrs.get_value("source") {
121 if let Ok(s) = value.as_text() {
122 cf.source = Some(s.to_string());
123 }
124 }
125
126 if let Some(value) = attrs.get_value("history") {
127 if let Ok(s) = value.as_text() {
128 cf.history = Some(s.to_string());
129 }
130 }
131
132 if let Some(value) = attrs.get_value("references") {
133 if let Ok(s) = value.as_text() {
134 cf.references = Some(s.to_string());
135 }
136 }
137
138 if let Some(value) = attrs.get_value("comment") {
139 if let Ok(s) = value.as_text() {
140 cf.comment = Some(s.to_string());
141 }
142 }
143
144 cf
145 }
146
147 pub fn to_attributes(&self) -> Attributes {
149 let mut attrs = Attributes::new();
150
151 if let Some(ref conventions) = self.conventions {
152 let _ = attrs.add(
153 Attribute::new("Conventions", AttributeValue::text(conventions.clone()))
154 .expect("Valid attribute"),
155 );
156 }
157
158 if let Some(ref title) = self.title {
159 let _ = attrs.add(
160 Attribute::new("title", AttributeValue::text(title.clone()))
161 .expect("Valid attribute"),
162 );
163 }
164
165 if let Some(ref institution) = self.institution {
166 let _ = attrs.add(
167 Attribute::new("institution", AttributeValue::text(institution.clone()))
168 .expect("Valid attribute"),
169 );
170 }
171
172 if let Some(ref source) = self.source {
173 let _ = attrs.add(
174 Attribute::new("source", AttributeValue::text(source.clone()))
175 .expect("Valid attribute"),
176 );
177 }
178
179 if let Some(ref history) = self.history {
180 let _ = attrs.add(
181 Attribute::new("history", AttributeValue::text(history.clone()))
182 .expect("Valid attribute"),
183 );
184 }
185
186 if let Some(ref references) = self.references {
187 let _ = attrs.add(
188 Attribute::new("references", AttributeValue::text(references.clone()))
189 .expect("Valid attribute"),
190 );
191 }
192
193 if let Some(ref comment) = self.comment {
194 let _ = attrs.add(
195 Attribute::new("comment", AttributeValue::text(comment.clone()))
196 .expect("Valid attribute"),
197 );
198 }
199
200 attrs
201 }
202
203 #[must_use]
205 pub fn has_conventions(&self) -> bool {
206 self.conventions.is_some()
207 }
208
209 #[must_use]
211 pub fn is_cf_compliant(&self) -> bool {
212 self.conventions
213 .as_ref()
214 .is_some_and(|c| c.starts_with("CF-"))
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct NetCdfMetadata {
221 version: NetCdfVersion,
223 global_attributes: Attributes,
225 dimensions: Dimensions,
227 variables: Variables,
229 cf_metadata: Option<CfMetadata>,
231}
232
233impl NetCdfMetadata {
234 pub fn new(version: NetCdfVersion) -> Self {
240 Self {
241 version,
242 global_attributes: Attributes::new(),
243 dimensions: Dimensions::new(),
244 variables: Variables::new(),
245 cf_metadata: None,
246 }
247 }
248
249 #[must_use]
251 pub fn new_classic() -> Self {
252 Self::new(NetCdfVersion::Classic)
253 }
254
255 #[must_use]
257 pub fn new_netcdf4() -> Self {
258 Self::new(NetCdfVersion::NetCdf4)
259 }
260
261 #[must_use]
263 pub const fn version(&self) -> NetCdfVersion {
264 self.version
265 }
266
267 #[must_use]
269 pub const fn global_attributes(&self) -> &Attributes {
270 &self.global_attributes
271 }
272
273 pub fn global_attributes_mut(&mut self) -> &mut Attributes {
275 &mut self.global_attributes
276 }
277
278 #[must_use]
280 pub const fn dimensions(&self) -> &Dimensions {
281 &self.dimensions
282 }
283
284 pub fn dimensions_mut(&mut self) -> &mut Dimensions {
286 &mut self.dimensions
287 }
288
289 #[must_use]
291 pub const fn variables(&self) -> &Variables {
292 &self.variables
293 }
294
295 pub fn variables_mut(&mut self) -> &mut Variables {
297 &mut self.variables
298 }
299
300 #[must_use]
302 pub const fn cf_metadata(&self) -> Option<&CfMetadata> {
303 self.cf_metadata.as_ref()
304 }
305
306 pub fn set_cf_metadata(&mut self, cf: CfMetadata) {
308 self.cf_metadata = Some(cf);
309 }
310
311 pub fn parse_cf_metadata(&mut self) {
313 let cf = CfMetadata::from_attributes(&self.global_attributes);
314 if cf.has_conventions() {
315 self.cf_metadata = Some(cf);
316 }
317 }
318
319 pub fn validate(&self) -> Result<()> {
325 for var in self.variables.iter() {
327 for dim_name in var.dimension_names() {
328 if !self.dimensions.contains(dim_name) {
329 return Err(NetCdfError::DimensionNotFound {
330 name: dim_name.clone(),
331 });
332 }
333 }
334
335 if self.version.is_netcdf3() && !var.is_netcdf3_compatible() {
337 return Err(NetCdfError::VariableError(format!(
338 "Variable '{}' uses data type '{}' which is not compatible with NetCDF-3",
339 var.name(),
340 var.data_type().name()
341 )));
342 }
343 }
344
345 if self.version.is_netcdf3() {
347 let unlimited_count = self.dimensions.iter().filter(|d| d.is_unlimited()).count();
348 if unlimited_count > 1 {
349 return Err(NetCdfError::UnlimitedDimensionError(
350 "NetCDF-3 can only have one unlimited dimension".to_string(),
351 ));
352 }
353 }
354
355 Ok(())
356 }
357
358 #[must_use]
360 pub fn summary(&self) -> String {
361 format!(
362 "NetCDF {} file with {} dimensions, {} variables, {} global attributes",
363 self.version.format_name(),
364 self.dimensions.len(),
365 self.variables.len(),
366 self.global_attributes.len()
367 )
368 }
369}
370
371impl Default for NetCdfMetadata {
372 fn default() -> Self {
373 Self::new_classic()
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::dimension::Dimension;
381 use crate::variable::{DataType, Variable};
382
383 #[test]
384 fn test_netcdf_version() {
385 assert!(NetCdfVersion::Classic.is_netcdf3());
386 assert!(!NetCdfVersion::Classic.is_netcdf4());
387 assert_eq!(NetCdfVersion::Classic.version_number(), 3);
388
389 assert!(NetCdfVersion::NetCdf4.is_netcdf4());
390 assert!(!NetCdfVersion::NetCdf4.is_netcdf3());
391 assert_eq!(NetCdfVersion::NetCdf4.version_number(), 4);
392 }
393
394 #[test]
395 fn test_cf_metadata() {
396 let mut cf = CfMetadata::new();
397 cf.conventions = Some("CF-1.8".to_string());
398 cf.title = Some("Test Dataset".to_string());
399
400 assert!(cf.has_conventions());
401 assert!(cf.is_cf_compliant());
402
403 let attrs = cf.to_attributes();
404 assert_eq!(attrs.len(), 2);
405 assert!(attrs.contains("Conventions"));
406 assert!(attrs.contains("title"));
407 }
408
409 #[test]
410 fn test_cf_from_attributes() {
411 let mut attrs = Attributes::new();
412 attrs
413 .add(
414 Attribute::new("Conventions", AttributeValue::text("CF-1.8"))
415 .expect("Failed to create Conventions attribute"),
416 )
417 .expect("Failed to add Conventions attribute");
418 attrs
419 .add(
420 Attribute::new("title", AttributeValue::text("Test"))
421 .expect("Failed to create title attribute"),
422 )
423 .expect("Failed to add title attribute");
424
425 let cf = CfMetadata::from_attributes(&attrs);
426 assert_eq!(cf.conventions.as_deref(), Some("CF-1.8"));
427 assert_eq!(cf.title.as_deref(), Some("Test"));
428 }
429
430 #[test]
431 fn test_metadata_creation() {
432 let mut metadata = NetCdfMetadata::new_classic();
433 assert_eq!(metadata.version(), NetCdfVersion::Classic);
434
435 metadata
436 .dimensions_mut()
437 .add(Dimension::new("time", 10).expect("Failed to create time dimension"))
438 .expect("Failed to add time dimension");
439 metadata
440 .variables_mut()
441 .add(
442 Variable::new_coordinate("time", DataType::F64)
443 .expect("Failed to create time variable"),
444 )
445 .expect("Failed to add time variable");
446
447 assert_eq!(metadata.dimensions().len(), 1);
448 assert_eq!(metadata.variables().len(), 1);
449 }
450
451 #[test]
452 fn test_metadata_validation() {
453 let mut metadata = NetCdfMetadata::new_classic();
454 metadata
455 .dimensions_mut()
456 .add(Dimension::new("time", 10).expect("Failed to create time dimension"))
457 .expect("Failed to add time dimension");
458 metadata
459 .variables_mut()
460 .add(
461 Variable::new_coordinate("time", DataType::F64)
462 .expect("Failed to create time variable"),
463 )
464 .expect("Failed to add time variable");
465
466 assert!(metadata.validate().is_ok());
467 }
468
469 #[test]
470 fn test_metadata_validation_missing_dimension() {
471 let mut metadata = NetCdfMetadata::new_classic();
472 metadata
473 .variables_mut()
474 .add(
475 Variable::new("temp", DataType::F32, vec!["time".to_string()])
476 .expect("Failed to create temp variable"),
477 )
478 .expect("Failed to add temp variable");
479
480 let result = metadata.validate();
481 assert!(result.is_err());
482 }
483
484 #[test]
485 fn test_netcdf3_type_validation() {
486 let mut metadata = NetCdfMetadata::new_classic();
487 metadata
488 .dimensions_mut()
489 .add(Dimension::new("x", 10).expect("Failed to create x dimension"))
490 .expect("Failed to add x dimension");
491 metadata
492 .variables_mut()
493 .add(
494 Variable::new("data", DataType::U16, vec!["x".to_string()])
495 .expect("Failed to create data variable"),
496 )
497 .expect("Failed to add data variable");
498
499 let result = metadata.validate();
500 assert!(result.is_err());
501 }
502
503 #[test]
504 fn test_unlimited_dimension_validation() {
505 let mut metadata = NetCdfMetadata::new_classic();
506 metadata
507 .dimensions_mut()
508 .add(Dimension::new_unlimited("time", 10).expect("Failed to create time dimension"))
509 .expect("Failed to add time dimension");
510 metadata
511 .dimensions_mut()
512 .add(Dimension::new_unlimited("level", 5).expect("Failed to create level dimension"))
513 .expect("Failed to add level dimension");
514
515 let result = metadata.validate();
516 assert!(result.is_err());
517 }
518
519 #[test]
520 fn test_summary() {
521 let mut metadata = NetCdfMetadata::new_classic();
522 metadata
523 .dimensions_mut()
524 .add(Dimension::new("time", 10).expect("Failed to create time dimension"))
525 .expect("Failed to add time dimension");
526 metadata
527 .variables_mut()
528 .add(
529 Variable::new_coordinate("time", DataType::F64)
530 .expect("Failed to create time variable"),
531 )
532 .expect("Failed to add time variable");
533
534 let summary = metadata.summary();
535 assert!(summary.contains("NetCDF-3"));
536 assert!(summary.contains("1 dimensions"));
537 assert!(summary.contains("1 variables"));
538 }
539}