use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use hashbrown::HashSet;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct CatalogDto {
pub(super) observatory: Vec<ObservatoryDto>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ObservatoryDto {
pub(super) name: String,
pub(super) longitude_deg: f64,
pub(super) latitude_deg: f64,
pub(super) height_m: f64,
pub(super) reference_pressure_hpa: f64,
pub(super) reference_temperature_k: Option<f64>,
pub(super) reference_relative_humidity: Option<f64>,
}
#[derive(Debug)]
pub(super) struct ValidationError {
pub(super) record: usize,
pub(super) name: String,
pub(super) field: &'static str,
pub(super) reason: String,
}
impl fmt::Display for ValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"invalid observatory record {} (`{}`), field `{}`: {}",
self.record, self.name, self.field, self.reason
)
}
}
pub(super) fn parse_catalog(input: &str) -> Result<CatalogDto, toml::de::Error> {
toml::from_str(input)
}
pub(super) fn validate_catalog(catalog: &CatalogDto) -> Result<(), ValidationError> {
let mut names = HashSet::new();
for (index, record) in catalog.observatory.iter().enumerate() {
let record_number = index + 1;
validate_record(record_number, record)?;
if !names.insert(record.name.as_str()) {
return Err(ValidationError {
record: record_number,
name: record.name.clone(),
field: "name",
reason: "duplicate observatory name".into(),
});
}
}
Ok(())
}
fn validate_record(record_number: usize, record: &ObservatoryDto) -> Result<(), ValidationError> {
let display_name = if record.name.trim().is_empty() {
"<empty>"
} else {
record.name.as_str()
};
if record.name.trim().is_empty() {
return Err(ValidationError {
record: record_number,
name: display_name.to_owned(),
field: "name",
reason: "must not be empty".into(),
});
}
validate_range(
record_number,
display_name,
"longitude_deg",
record.longitude_deg,
-180.0,
180.0,
)?;
validate_range(
record_number,
display_name,
"latitude_deg",
record.latitude_deg,
-90.0,
90.0,
)?;
validate_range(
record_number,
display_name,
"height_m",
record.height_m,
-500.0,
10_000.0,
)?;
validate_range(
record_number,
display_name,
"reference_pressure_hpa",
record.reference_pressure_hpa,
f64::MIN_POSITIVE,
1_100.0,
)?;
if let Some(value) = record.reference_temperature_k {
validate_range(
record_number,
display_name,
"reference_temperature_k",
value,
f64::MIN_POSITIVE,
400.0,
)?;
}
if let Some(value) = record.reference_relative_humidity {
validate_range(
record_number,
display_name,
"reference_relative_humidity",
value,
0.0,
1.0,
)?;
}
Ok(())
}
fn validate_range(
record: usize,
name: &str,
field: &'static str,
value: f64,
min: f64,
max: f64,
) -> Result<(), ValidationError> {
if !value.is_finite() || value < min || value > max {
return Err(ValidationError {
record,
name: name.to_owned(),
field,
reason: format!("must be finite and in [{min}, {max}], got {value}"),
});
}
Ok(())
}