weather_dot_gov_rs/src/dwml.rs

248 lines
6.3 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 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>,
}
impl TimeLayout {
/// For a given time, returns the index of the interval that the time falls in.
fn lookup(&self, t: &chrono::DateTime<FixedOffset>) -> Option<usize> {
for i in 0..self.start_valid_time.len() - 1 {
let Some(start) = self.start_valid_time.get(i) else {
break;
};
let Some(stop) = self.start_valid_time.get(i + 1) else {
break;
};
if start.value.0 <= *t && *t < stop.value.0 {
return Some(i);
}
}
None
}
}
#[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,
}
#[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, Temperature),
#[serde(rename = "probability-of-precipitation")]
pub probability_of_precipitation: ProbabilityOfPrecipitation,
pub weather: Weather,
}
#[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>,
}
#[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(Debug, Deserialize, PartialEq)]
pub enum TempType {
#[serde(rename = "apparent")]
Apparent,
#[serde(rename = "dew point")]
DewPoint,
#[serde(rename = "minimum")]
Minimum,
#[serde(rename = "maximum")]
Maximum,
}
#[derive(Debug, Deserialize, PartialEq)]
pub enum TempUnits {
Celsius,
Fahrenheit,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct TimeLayoutKey(String);
#[cfg(test)]
mod tests {
use anyhow::{Context as _, Result};
use super::*;
#[test]
fn empire_city() {
let s = include_str!("../data/empire_city.xml");
let dwml: Dwml = quick_xml::de::from_str(s).unwrap();
for (input, expected) in [
("2025-01-14T18:59:59-05:00", None),
("2025-01-14T19:00:00-05:00", Some(0)),
("2025-01-21T05:59:59-05:00", Some(12)),
("2025-01-21T06:00:00-05:00", None),
] {
let dt = chrono::DateTime::parse_from_rfc3339(input).unwrap();
let actual = dwml.data.0.time_layout.0.lookup(&dt);
assert_eq!(actual, expected);
}
}
#[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"
] {
let _ = chrono::DateTime::parse_from_rfc3339(&s).context(s).unwrap();
}
Ok(())
}
}