248 lines
6.3 KiB
Rust
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(())
|
|
}
|
|
}
|