weather_dot_gov_rs/src/dwml.rs

295 lines
7.1 KiB
Rust

//! 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<Self, quick_xml::DeError> {
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<String>,
pub description: Option<String>,
}
#[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<StartValidTime>,
}
#[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<FixedOffset>);
impl TryFrom<String> for DateTime {
type Error = chrono::ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
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<i32>,
}
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<PrecipitationValue>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct Weather {
#[serde(rename = "@time-layout")]
pub time_layout: TimeLayoutKey,
#[serde(rename = "weather-conditions")]
pub weather_conditions: Vec<WeatherCondition>,
}
#[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<u32>,
}
#[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(())
}
}