use serde::{Deserialize, Serialize};
use crate::attribute::Attributes;
use crate::dimension::Dimensions;
use crate::variable::{DataType, Variable, Variables};
mod coordinates;
mod metadata;
#[cfg(test)]
mod tests;
mod time;
pub mod v1_11;
pub use coordinates::{AxisType, CoordinateDetector, GridMapping, GridMappingType};
pub use metadata::{
CellMeasure, CellMeasureType, CellMethod, CellMethodOperation, StandardNameEntry,
StandardNameTable, UnitsValidator,
};
pub use time::BoundsVariable;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum CfComplianceLevel {
Required,
Recommended,
Optional,
}
impl CfComplianceLevel {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Required => "Required",
Self::Recommended => "Recommended",
Self::Optional => "Optional",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CfIssueType {
MissingAttribute,
InvalidAttributeValue,
InvalidStandardName,
InvalidUnits,
UnitsMismatch,
InvalidCellMethods,
MissingBounds,
InvalidBounds,
InvalidGridMapping,
MissingCoordinateVariable,
InvalidCoordinateVariable,
InvalidCellMeasures,
ConventionIssue,
}
impl CfIssueType {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::MissingAttribute => "Missing Attribute",
Self::InvalidAttributeValue => "Invalid Attribute Value",
Self::InvalidStandardName => "Invalid Standard Name",
Self::InvalidUnits => "Invalid Units",
Self::UnitsMismatch => "Units Mismatch",
Self::InvalidCellMethods => "Invalid Cell Methods",
Self::MissingBounds => "Missing Bounds",
Self::InvalidBounds => "Invalid Bounds",
Self::InvalidGridMapping => "Invalid Grid Mapping",
Self::MissingCoordinateVariable => "Missing Coordinate Variable",
Self::InvalidCoordinateVariable => "Invalid Coordinate Variable",
Self::InvalidCellMeasures => "Invalid Cell Measures",
Self::ConventionIssue => "Convention Issue",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CfValidationIssue {
pub level: CfComplianceLevel,
pub issue_type: CfIssueType,
pub variable: Option<String>,
pub attribute: Option<String>,
pub message: String,
pub section: Option<String>,
}
impl CfValidationIssue {
#[must_use]
pub fn new(
level: CfComplianceLevel,
issue_type: CfIssueType,
message: impl Into<String>,
) -> Self {
Self {
level,
issue_type,
variable: None,
attribute: None,
message: message.into(),
section: None,
}
}
#[must_use]
pub fn with_variable(mut self, variable: impl Into<String>) -> Self {
self.variable = Some(variable.into());
self
}
#[must_use]
pub fn with_attribute(mut self, attribute: impl Into<String>) -> Self {
self.attribute = Some(attribute.into());
self
}
#[must_use]
pub fn with_section(mut self, section: impl Into<String>) -> Self {
self.section = Some(section.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CfValidationReport {
issues: Vec<CfValidationIssue>,
cf_version: String,
}
impl CfValidationReport {
#[must_use]
pub fn new(cf_version: impl Into<String>) -> Self {
Self {
issues: Vec::new(),
cf_version: cf_version.into(),
}
}
pub fn add_issue(&mut self, issue: CfValidationIssue) {
self.issues.push(issue);
}
#[must_use]
pub fn issues(&self) -> &[CfValidationIssue] {
&self.issues
}
pub fn issues_at_level(
&self,
level: CfComplianceLevel,
) -> impl Iterator<Item = &CfValidationIssue> {
self.issues.iter().filter(move |i| i.level == level)
}
#[must_use]
pub fn is_compliant(&self, level: CfComplianceLevel) -> bool {
!self.issues.iter().any(|i| i.level <= level)
}
#[must_use]
pub fn count_at_level(&self, level: CfComplianceLevel) -> usize {
self.issues.iter().filter(|i| i.level == level).count()
}
#[must_use]
pub fn cf_version(&self) -> &str {
&self.cf_version
}
#[must_use]
pub fn summary(&self) -> String {
let required = self.count_at_level(CfComplianceLevel::Required);
let recommended = self.count_at_level(CfComplianceLevel::Recommended);
let optional = self.count_at_level(CfComplianceLevel::Optional);
format!(
"CF-{} Validation: {} required, {} recommended, {} optional issues",
self.cf_version, required, recommended, optional
)
}
}
#[derive(Debug)]
pub struct CfValidator {
standard_names: StandardNameTable,
units_validator: UnitsValidator,
coordinate_detector: CoordinateDetector,
cf_version: String,
}
impl Default for CfValidator {
fn default() -> Self {
Self::new()
}
}
impl CfValidator {
#[must_use]
pub fn new() -> Self {
Self {
standard_names: StandardNameTable::cf_1_8(),
units_validator: UnitsValidator::new(),
coordinate_detector: CoordinateDetector::new(),
cf_version: "1.8".to_string(),
}
}
pub fn set_version(&mut self, version: impl Into<String>) {
self.cf_version = version.into();
}
pub fn validate_global_attributes(&self, attrs: &Attributes, report: &mut CfValidationReport) {
if !attrs.contains("Conventions") {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Required,
CfIssueType::MissingAttribute,
"Missing 'Conventions' global attribute",
)
.with_attribute("Conventions")
.with_section("2.6.1"),
);
} else if let Some(attr) = attrs.get("Conventions") {
if let Ok(conv) = attr.value().as_text() {
if !conv.contains("CF-") {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Required,
CfIssueType::InvalidAttributeValue,
format!("Conventions attribute '{}' does not contain 'CF-'", conv),
)
.with_attribute("Conventions")
.with_section("2.6.1"),
);
}
}
}
let recommended = ["title", "institution", "source", "history", "references"];
for attr_name in recommended {
if !attrs.contains(attr_name) {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Recommended,
CfIssueType::MissingAttribute,
format!("Missing recommended global attribute '{}'", attr_name),
)
.with_attribute(attr_name)
.with_section("2.6.2"),
);
}
}
}
pub fn validate_variable(
&self,
var: &Variable,
dimensions: &Dimensions,
report: &mut CfValidationReport,
) {
let var_name = var.name();
let attrs = var.attributes();
if let Some(attr) = attrs.get("standard_name") {
if let Ok(std_name) = attr.value().as_text() {
if !self.standard_names.contains(std_name) {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Recommended,
CfIssueType::InvalidStandardName,
format!("Unknown standard_name '{}'", std_name),
)
.with_variable(var_name)
.with_attribute("standard_name")
.with_section("3.3"),
);
}
if let Some(canonical) = self.standard_names.canonical_units(std_name) {
if let Some(units_attr) = attrs.get("units") {
if let Ok(units) = units_attr.value().as_text() {
if !self.units_validator.are_compatible(units, canonical) {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Required,
CfIssueType::UnitsMismatch,
format!(
"Units '{}' not compatible with canonical units '{}' for standard_name '{}'",
units, canonical, std_name
),
)
.with_variable(var_name)
.with_section("3.1"),
);
}
}
}
}
}
}
if !self
.coordinate_detector
.is_coordinate_variable(var, dimensions)
&& !attrs.contains("units")
&& var.data_type() != DataType::Char
&& var.data_type() != DataType::String
{
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Recommended,
CfIssueType::MissingAttribute,
format!("Variable '{}' missing 'units' attribute", var_name),
)
.with_variable(var_name)
.with_section("3.1"),
);
}
if let Some(units_attr) = attrs.get("units") {
if let Ok(units) = units_attr.value().as_text() {
if !self.units_validator.is_valid(units) {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Recommended,
CfIssueType::InvalidUnits,
format!("Invalid units '{}' for variable '{}'", units, var_name),
)
.with_variable(var_name)
.with_attribute("units")
.with_section("3.1"),
);
}
}
}
if let Some(cm_attr) = attrs.get("cell_methods") {
if let Ok(cell_methods) = cm_attr.value().as_text() {
if let Err(e) = CellMethod::parse_cell_methods(cell_methods) {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Required,
CfIssueType::InvalidCellMethods,
format!("Invalid cell_methods '{}': {}", cell_methods, e),
)
.with_variable(var_name)
.with_attribute("cell_methods")
.with_section("7.3"),
);
}
}
}
if let Some(bounds_attr) = attrs.get("bounds") {
if let Ok(_bounds_var_name) = bounds_attr.value().as_text() {
}
}
if let Some(cm_attr) = attrs.get("cell_measures") {
if let Ok(cell_measures) = cm_attr.value().as_text() {
if let Err(e) = CellMeasure::parse_cell_measures(cell_measures) {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Required,
CfIssueType::InvalidCellMeasures,
format!("Invalid cell_measures '{}': {}", cell_measures, e),
)
.with_variable(var_name)
.with_attribute("cell_measures")
.with_section("7.2"),
);
}
}
}
}
pub fn validate_grid_mapping(
&self,
var: &Variable,
variables: &Variables,
report: &mut CfValidationReport,
) {
let var_name = var.name();
if let Some(gm_attr) = var.attributes().get("grid_mapping") {
if let Ok(gm_var_name) = gm_attr.value().as_text() {
if let Some(gm_var) = variables.get(gm_var_name) {
if let Some(gmn_attr) = gm_var.attributes().get("grid_mapping_name") {
if let Ok(gm_name) = gmn_attr.value().as_text() {
let gm_type = GridMappingType::from_cf_name(gm_name);
if gm_type == GridMappingType::Unknown {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Recommended,
CfIssueType::InvalidGridMapping,
format!("Unknown grid_mapping_name '{}'", gm_name),
)
.with_variable(gm_var_name)
.with_section("5.6"),
);
}
}
} else {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Required,
CfIssueType::InvalidGridMapping,
format!("Grid mapping variable '{}' missing 'grid_mapping_name' attribute", gm_var_name),
)
.with_variable(gm_var_name)
.with_section("5.6"),
);
}
} else {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Required,
CfIssueType::InvalidGridMapping,
format!(
"Grid mapping variable '{}' referenced by '{}' not found",
gm_var_name, var_name
),
)
.with_variable(var_name)
.with_section("5.6"),
);
}
}
}
}
pub fn validate_coordinates(
&self,
dimensions: &Dimensions,
variables: &Variables,
report: &mut CfValidationReport,
) {
for dim in dimensions.iter() {
let dim_name = dim.name();
if variables.get(dim_name).is_none() {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Recommended,
CfIssueType::MissingCoordinateVariable,
format!(
"Dimension '{}' has no corresponding coordinate variable",
dim_name
),
)
.with_section("5.1"),
);
}
}
for var in variables.iter() {
if self
.coordinate_detector
.is_coordinate_variable(var, dimensions)
{
if var.attributes().get("axis").is_none() {
if let Some(_axis) = self.coordinate_detector.detect_axis(var) {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Recommended,
CfIssueType::MissingAttribute,
format!(
"Coordinate variable '{}' should have 'axis' attribute",
var.name()
),
)
.with_variable(var.name())
.with_section("4"),
);
}
}
}
}
}
pub fn validate_bounds(
&self,
variables: &Variables,
dimensions: &Dimensions,
report: &mut CfValidationReport,
) {
for var in variables.iter() {
if let Some(bounds_attr) = var.attributes().get("bounds") {
if let Ok(bounds_var_name) = bounds_attr.value().as_text() {
if let Some(bounds_var) = variables.get(bounds_var_name) {
let bounds = BoundsVariable::new(bounds_var_name, var.name(), 2);
if let Err(e) = bounds.validate(var, bounds_var, dimensions) {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Required,
CfIssueType::InvalidBounds,
format!("Invalid bounds variable '{}': {}", bounds_var_name, e),
)
.with_variable(var.name())
.with_section("7.1"),
);
}
} else {
report.add_issue(
CfValidationIssue::new(
CfComplianceLevel::Required,
CfIssueType::MissingBounds,
format!(
"Bounds variable '{}' referenced by '{}' not found",
bounds_var_name,
var.name()
),
)
.with_variable(var.name())
.with_section("7.1"),
);
}
}
}
}
}
pub fn validate(
&self,
global_attrs: &Attributes,
dimensions: &Dimensions,
variables: &Variables,
) -> CfValidationReport {
let mut report = CfValidationReport::new(&self.cf_version);
self.validate_global_attributes(global_attrs, &mut report);
for var in variables.iter() {
self.validate_variable(var, dimensions, &mut report);
self.validate_grid_mapping(var, variables, &mut report);
}
self.validate_coordinates(dimensions, variables, &mut report);
self.validate_bounds(variables, dimensions, &mut report);
report
}
}