use chrono::NaiveDate;
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Observation {
pub realtime_start: NaiveDate,
pub realtime_end: NaiveDate,
pub date: NaiveDate,
#[serde(deserialize_with = "deserialize_value")]
pub value: Option<f64>,
}
fn deserialize_value<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
if raw == "." {
return Ok(None);
}
raw.parse::<f64>()
.map(Some)
.map_err(serde::de::Error::custom)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_value_maps_to_none() {
let obs: Observation = serde_json::from_str(
r#"{"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1930-01-01","value":"."}"#,
)
.unwrap();
assert_eq!(obs.value, None);
assert_eq!(obs.date, NaiveDate::from_ymd_opt(1930, 1, 1).unwrap());
}
#[test]
fn numeric_value_parses() {
let obs: Observation = serde_json::from_str(
r#"{"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1929-01-01","value":"1065.9"}"#,
)
.unwrap();
assert_eq!(obs.value, Some(1065.9));
}
#[test]
fn realtime_period_deserializes() {
let obs: Observation = serde_json::from_str(
r#"{"realtime_start":"2020-03-26","realtime_end":"2021-03-25","date":"1929-01-01","value":"1120.076"}"#,
)
.unwrap();
assert_eq!(
obs.realtime_start,
NaiveDate::from_ymd_opt(2020, 3, 26).unwrap()
);
assert_eq!(
obs.realtime_end,
NaiveDate::from_ymd_opt(2021, 3, 25).unwrap()
);
assert_eq!(obs.value, Some(1120.076));
}
#[test]
fn unparseable_value_is_an_error() {
let parsed: Result<Observation, _> = serde_json::from_str(
r#"{"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1929-01-01","value":"not a number"}"#,
);
assert!(parsed.is_err());
}
#[test]
fn serializes_value_as_number_or_null() {
let today = NaiveDate::from_ymd_opt(2026, 7, 6).unwrap();
let present = Observation {
realtime_start: today,
realtime_end: today,
date: NaiveDate::from_ymd_opt(1929, 1, 1).unwrap(),
value: Some(1065.9),
};
assert_eq!(
serde_json::to_value(&present).unwrap(),
serde_json::json!({
"realtime_start": "2026-07-06", "realtime_end": "2026-07-06",
"date": "1929-01-01", "value": 1065.9
})
);
let missing = Observation {
realtime_start: today,
realtime_end: today,
date: NaiveDate::from_ymd_opt(1930, 1, 1).unwrap(),
value: None,
};
assert_eq!(
serde_json::to_value(&missing).unwrap(),
serde_json::json!({
"realtime_start": "2026-07-06", "realtime_end": "2026-07-06",
"date": "1930-01-01", "value": null
})
);
}
}