ferric_fred/
observation.rs1use chrono::NaiveDate;
2use serde::{Deserialize, Deserializer, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22pub struct Observation {
23 pub realtime_start: NaiveDate,
26
27 pub realtime_end: NaiveDate,
30
31 pub date: NaiveDate,
34
35 #[serde(deserialize_with = "deserialize_value")]
37 pub value: Option<f64>,
38}
39
40fn deserialize_value<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
43where
44 D: Deserializer<'de>,
45{
46 let raw = String::deserialize(deserializer)?;
47 if raw == "." {
48 return Ok(None);
49 }
50 raw.parse::<f64>()
51 .map(Some)
52 .map_err(serde::de::Error::custom)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn missing_value_maps_to_none() {
61 let obs: Observation = serde_json::from_str(
62 r#"{"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1930-01-01","value":"."}"#,
63 )
64 .unwrap();
65 assert_eq!(obs.value, None);
66 assert_eq!(obs.date, NaiveDate::from_ymd_opt(1930, 1, 1).unwrap());
67 }
68
69 #[test]
70 fn numeric_value_parses() {
71 let obs: Observation = serde_json::from_str(
72 r#"{"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1929-01-01","value":"1065.9"}"#,
73 )
74 .unwrap();
75 assert_eq!(obs.value, Some(1065.9));
76 }
77
78 #[test]
79 fn realtime_period_deserializes() {
80 let obs: Observation = serde_json::from_str(
82 r#"{"realtime_start":"2020-03-26","realtime_end":"2021-03-25","date":"1929-01-01","value":"1120.076"}"#,
83 )
84 .unwrap();
85 assert_eq!(
86 obs.realtime_start,
87 NaiveDate::from_ymd_opt(2020, 3, 26).unwrap()
88 );
89 assert_eq!(
90 obs.realtime_end,
91 NaiveDate::from_ymd_opt(2021, 3, 25).unwrap()
92 );
93 assert_eq!(obs.value, Some(1120.076));
94 }
95
96 #[test]
97 fn unparseable_value_is_an_error() {
98 let parsed: Result<Observation, _> = serde_json::from_str(
99 r#"{"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1929-01-01","value":"not a number"}"#,
100 );
101 assert!(parsed.is_err());
102 }
103
104 #[test]
105 fn serializes_value_as_number_or_null() {
106 let today = NaiveDate::from_ymd_opt(2026, 7, 6).unwrap();
107 let present = Observation {
108 realtime_start: today,
109 realtime_end: today,
110 date: NaiveDate::from_ymd_opt(1929, 1, 1).unwrap(),
111 value: Some(1065.9),
112 };
113 assert_eq!(
114 serde_json::to_value(&present).unwrap(),
115 serde_json::json!({
116 "realtime_start": "2026-07-06", "realtime_end": "2026-07-06",
117 "date": "1929-01-01", "value": 1065.9
118 })
119 );
120
121 let missing = Observation {
122 realtime_start: today,
123 realtime_end: today,
124 date: NaiveDate::from_ymd_opt(1930, 1, 1).unwrap(),
125 value: None,
126 };
127 assert_eq!(
128 serde_json::to_value(&missing).unwrap(),
129 serde_json::json!({
130 "realtime_start": "2026-07-06", "realtime_end": "2026-07-06",
131 "date": "1930-01-01", "value": null
132 })
133 );
134 }
135}