//! Encapsulates the DWML implementation details use chrono::FixedOffset; use serde::Deserialize; /// Top-level structure #[derive(Debug, Deserialize, PartialEq)] pub struct Dwml { pub head: Head, pub data: (Forecast, CurrentObservations), } impl Dwml { /// Returns the current "high" temperature. /// /// Panics if the data doesn't show a current high pub fn current_maximum(&self) -> TempWithUnit { self.forecast() .parameters .temp_by_type(TempType::Maximum) .first() } /// Returns the current "low" temperature. /// /// Panics if the data doesn't show a current low pub fn current_minimum(&self) -> TempWithUnit { self.forecast() .parameters .temp_by_type(TempType::Minimum) .first() } fn forecast(&self) -> &Forecast { &self.data.0 } } impl std::str::FromStr for Dwml { type Err = quick_xml::DeError; fn from_str(s: &str) -> Result { quick_xml::de::from_str(s) } } #[derive(Debug, Deserialize, PartialEq)] pub struct Head { pub product: Product, pub source: Source, } #[derive(Debug, Deserialize, PartialEq)] pub struct Forecast { pub location: Location, #[serde(rename = "time-layout")] pub time_layout: (TimeLayout, TimeLayout, TimeLayout), pub parameters: Parameters, } #[derive(Debug, Deserialize, PartialEq)] pub struct Location { #[serde(rename = "area-description")] pub area_description: Option, pub description: Option, } #[derive(Debug, Deserialize, PartialEq)] pub struct TimeLayout { #[serde(rename = "@time-coordinate")] pub time_coordinate: TimeCoordinate, #[serde(rename = "layout-key")] pub layout_key: TimeLayoutKey, #[serde(rename = "start-valid-time")] pub start_valid_time: Vec, } #[derive(Debug, Deserialize, PartialEq)] pub enum TimeCoordinate { #[serde(rename = "local")] Local, } #[derive(Debug, Deserialize, PartialEq)] pub struct StartValidTime { #[serde(rename = "@period-name")] pub period_name: String, #[serde(rename = "$value")] pub value: DateTime, } #[derive(Debug, Deserialize, PartialEq)] #[serde(try_from = "String")] pub struct DateTime(pub chrono::DateTime); impl TryFrom for DateTime { type Error = chrono::ParseError; fn try_from(s: String) -> Result { chrono::DateTime::parse_from_rfc3339(&s).map(Self) } } /// Observations at time of request /// /// Note that `time-layout` isn't parsed because in San Franscisco it returns a time with no timezone, which makes the schema too complicated. It's in local California time, so we could graft on a timezone from another part of the XML, but why bother. #[derive(Debug, Deserialize, PartialEq)] pub struct CurrentObservations { pub location: Location, pub parameters: CurrentParameters, } #[derive(Debug, Deserialize, PartialEq)] pub struct CurrentParameters { pub temperature: (Temperature, Temperature), pub humidity: Humidity, } #[derive(Debug, Deserialize, PartialEq)] pub struct Humidity { #[serde(rename = "@type")] pub humidity_type: HumidityType, pub value: u8, } #[derive(Debug, Deserialize, PartialEq)] pub enum HumidityType { #[serde(rename = "relative")] Relative, } #[derive(Debug, Deserialize, PartialEq)] pub struct Product { #[serde(rename = "creation-date")] pub creation_date: CreationDate, } #[derive(Debug, Deserialize, PartialEq)] pub struct CreationDate { #[serde(rename = "@refresh-frequency")] pub refresh_frequency: String, #[serde(rename = "$value")] pub value: String, } #[derive(Debug, Deserialize, PartialEq)] pub struct Source { #[serde(rename = "production-center")] pub production_center: String, } #[derive(Debug, Deserialize, PartialEq)] pub struct Parameters { pub temperature: [Temperature; 2], #[serde(rename = "probability-of-precipitation")] pub probability_of_precipitation: ProbabilityOfPrecipitation, pub weather: Weather, } impl Parameters { fn temp_by_type(&self, temp_type: TempType) -> &Temperature { for temp in &self.temperature { if temp.temp_type == temp_type { return temp; } } panic!("Couldn't find a Temperature with the given TempType"); } } pub struct TempWithUnit { pub units: TempUnits, pub value: i32, } #[derive(Debug, Deserialize, PartialEq)] pub struct Temperature { #[serde(rename = "@type")] pub temp_type: TempType, #[serde(rename = "@units")] pub units: TempUnits, #[serde(rename = "@time-layout")] pub time_layout: TimeLayoutKey, pub value: Vec, } impl Temperature { fn first(&self) -> TempWithUnit { let value = *self .value .first() .expect("temperatures should always have at least one value"); TempWithUnit { units: self.units, value, } } } #[derive(Debug, Deserialize, PartialEq)] pub struct ProbabilityOfPrecipitation { #[serde(rename = "@time-layout")] pub time_layout: TimeLayoutKey, pub value: Vec, } #[derive(Debug, Deserialize, PartialEq)] pub struct Weather { #[serde(rename = "@time-layout")] pub time_layout: TimeLayoutKey, #[serde(rename = "weather-conditions")] pub weather_conditions: Vec, } #[derive(Debug, Deserialize, PartialEq)] pub struct WeatherCondition { /// e.g. "Showers Likely" or "Partly Sunny" #[serde(rename = "@weather-summary")] pub weather_summary: String, } // If only there was a percentage that could convey the concept of // no precipitation. #[derive(Debug, Deserialize, PartialEq)] pub struct PrecipitationValue { #[serde(rename = "$text")] pub value: Option, } #[derive(Clone, Copy, Debug, Deserialize, PartialEq)] pub enum TempType { #[serde(rename = "apparent")] Apparent, #[serde(rename = "dew point")] DewPoint, #[serde(rename = "minimum")] Minimum, #[serde(rename = "maximum")] Maximum, } #[derive(Clone, Copy, Debug, Deserialize, PartialEq)] pub enum TempUnits { Celsius, Fahrenheit, } #[derive(Debug, Deserialize, PartialEq)] pub struct TimeLayoutKey(String); #[cfg(test)] mod tests { use super::*; use anyhow::{Context as _, Result}; #[test] fn empire_city() { let s = include_str!("../data/empire_city.xml"); let dwml: Dwml = quick_xml::de::from_str(s).unwrap(); assert_eq!(dwml.current_maximum().value, 33); assert_eq!(dwml.current_minimum().value, 26); } #[test] fn sunset_city() { let s = include_str!("../data/sunset_city.xml"); let _: Dwml = quick_xml::de::from_str(s).unwrap(); } #[test] fn windy_city() { let s = include_str!("../data/windy_city.xml"); let _: Dwml = quick_xml::de::from_str(s).unwrap(); } #[test] fn date() -> Result<()> { for s in ["2025-01-01T23:00:00-00:00", "2025-01-01T23:00:00-00:00"] { let _ = chrono::DateTime::parse_from_rfc3339(s).context(s).unwrap(); } Ok(()) } }