brtsky 0.1.1

A rusty parser for the Bright Sky weather API found at https://brightsky.dev
Documentation
#[macro_use]
extern crate serde_derive;

use chrono::{DateTime, FixedOffset};
use serde_json::Value;
use std::fmt;

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Response {
    weather: Vec<WeatherData>,
    sources: Vec<Source>,
}

impl Response {
    pub fn weather_data(&self) -> &Vec<WeatherData> {
        &self.weather
    }

    pub fn sources(&self) -> &Vec<Source> {
        &self.sources
    }

    pub fn weather_data_sets(&self) -> impl Iterator<Item = WeatherDataSet> {
        let iter = self.weather.iter();
        let mapped = iter.filter_map(move |wd| WeatherDataSet::new(wd, wd.source(&self.sources)));
        mapped
    }
}

impl fmt::Display for Response {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let data_sets = self.weather_data_sets();
        let folded = data_sets
            .fold(None, |a, b| match a {
                Some(a) => Some(format!("{}\n\n{}", a, b)),
                None => Some(b.to_string()),
            })
            .unwrap_or_else(|| "[no data]".to_owned());
        write!(f, "{}", folded)
    }
}

impl std::str::FromStr for Response {
    type Err = serde_json::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        serde_json::from_str(s)
    }
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct WeatherData {
    #[serde(with = "date_serde")]
    pub timestamp: DateTime<FixedOffset>,
    pub source_id: i32,
    pub precipitation: Option<f32>,
    pub pressure_msl: Option<f32>,
    pub sunshine: Option<f32>,
    pub temperature: Option<f32>,
    pub wind_direction: Option<f32>,
    pub wind_speed: Option<f32>,
    pub cloud_cover: Option<f32>,
    pub dew_point: Option<f32>,
    pub relative_humidity: Option<f32>,
    pub visibility: Option<f32>,
    pub wind_gust_direction: Option<f32>,
    pub wind_gust_speed: Option<f32>,
    pub condition: Option<Condition>,
    pub icon: Option<String>,
    pub fallback_source_ids: Option<Value>,
}

impl WeatherData {
    fn source<'a>(&self, sources: &'a Vec<Source>) -> Option<&'a Source> {
        let time = &self.timestamp;
        for source in sources {
            if source.contains(time) {
                return Some(source);
            }
        }
        None
    }
}

pub struct WeatherDataSet<'a> {
    weather_data: &'a WeatherData,
    source: &'a Source,
}

impl<'a> WeatherDataSet<'a> {
    fn new(
        weather_data: &'a WeatherData,
        source: Option<&'a Source>,
    ) -> Option<WeatherDataSet<'a>> {
        source.map(|source| WeatherDataSet {
            weather_data,
            source,
        })
    }

    pub fn weather_data(&self) -> &WeatherData {
        &self.weather_data
    }

    pub fn source(&self) -> &Source {
        &self.source
    }
}

impl<'a> fmt::Display for WeatherDataSet<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "City: {}
Date: {}
Type: {}
Condition: {}
Temperature: {}
Sunshine: {}
Precipitation: {}
Wind Speed: {}
Wind Gust Speed: {}
Cloud Cover: {}
Humidity: {}",
            self.source.station_name,
            self.weather_data.timestamp,
            self.source.observation_type,
            &self
                .weather_data
                .condition
                .as_ref()
                .map_or_else(|| "-".to_owned(), |f| format!("{}", f)),
            self.weather_data
                .temperature
                .map_or_else(|| "-".to_owned(), |f| format!("{} °C", f)),
            self.weather_data
                .sunshine
                .map_or_else(|| "-".to_owned(), |f| format!("{} min", f)),
            self.weather_data
                .precipitation
                .map_or_else(|| "-".to_owned(), |f| format!("{} mm", f)),
            self.weather_data
                .wind_speed
                .map_or_else(|| "-".to_owned(), |f| format!("{} km/h", f)),
            self.weather_data
                .wind_gust_speed
                .map_or_else(|| "-".to_owned(), |f| format!("{} km/h", f)),
            self.weather_data
                .cloud_cover
                .map_or_else(|| "-".to_owned(), |f| format!("{} %", f)),
            self.weather_data
                .relative_humidity
                .map_or_else(|| "-".to_owned(), |f| format!("{} %", f))
        )
    }
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Source {
    pub id: i32,
    pub dwd_station_id: Option<String>,
    pub observation_type: ObservationType,
    pub lat: f32,
    pub lon: f32,
    pub height: f32,
    pub station_name: String,
    pub wmo_station_id: String,
    #[serde(with = "date_serde")]
    pub first_record: DateTime<FixedOffset>,
    #[serde(with = "date_serde")]
    pub last_record: DateTime<FixedOffset>,
    pub distance: f32,
}

impl Source {
    fn contains(&self, time: &DateTime<FixedOffset>) -> bool {
        &self.first_record <= time && time <= &self.last_record
    }
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub enum Condition {
    #[serde(rename = "dry")]
    Dry,
    #[serde(rename = "fog")]
    Fog,
    #[serde(rename = "rain")]
    Rain,
    #[serde(rename = "sleet")]
    Sleet,
    #[serde(rename = "snow")]
    Snow,
    #[serde(rename = "hail")]
    Hail,
    #[serde(rename = "thunderstorm")]
    Thunderstorm,
    #[serde(rename = "null")]
    Null,
}

impl fmt::Display for Condition {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Condition::Dry => write!(f, "Dry"),
            Condition::Fog => write!(f, "Fog"),
            Condition::Rain => write!(f, "Rain"),
            Condition::Sleet => write!(f, "Sleet"),
            Condition::Snow => write!(f, "Snow"),
            Condition::Hail => write!(f, "Hail"),
            Condition::Thunderstorm => write!(f, "Thunder Storm"),
            Condition::Null => write!(f, "-"),
        }
    }
}

#[derive(Debug, PartialEq, Deserialize)]
pub enum Icon {
    #[serde(rename = "clear-day")]
    ClearDay,
    #[serde(rename = "clear-night")]
    ClearNight,
    #[serde(rename = "partly-cloudy-day")]
    PartlyCloudyDay,
    #[serde(rename = "partly-cloudy-night")]
    PartlyCloudyNight,
    #[serde(rename = "cloudy")]
    Cloudy,
    #[serde(rename = "fog")]
    Fog,
    #[serde(rename = "wind")]
    Wind,
    #[serde(rename = "rain")]
    Rain,
    #[serde(rename = "sleet")]
    Sleet,
    #[serde(rename = "snow")]
    Snow,
    #[serde(rename = "hail")]
    Hail,
    #[serde(rename = "thunderstorm")]
    Thunderstorm,
    #[serde(rename = "null")]
    Null,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub enum ObservationType {
    #[serde(rename = "forecast")]
    Forecast,
    #[serde(rename = "synop")]
    Synop,
    #[serde(rename = "current")]
    Current,
    #[serde(rename = "recent")]
    Recent,
    #[serde(rename = "historical")]
    Historical,
}

impl fmt::Display for ObservationType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ObservationType::Forecast => write!(f, "Forecast"),
            ObservationType::Synop => write!(f, "SYNOP"),
            ObservationType::Current => write!(f, "Current"),
            ObservationType::Recent => write!(f, "Recent"),
            ObservationType::Historical => write!(f, "Historical"),
        }
    }
}

mod date_serde {
    use chrono::{DateTime, FixedOffset};
    use serde::{self, Deserialize, Deserializer};

    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<FixedOffset>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s: String = String::deserialize(deserializer)?;
        Ok(DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom)?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse() {
        let data = std::fs::read("test/test.json").unwrap();
        let data: Response = serde_json::from_slice(&data).unwrap();
        let timestamp = data.weather[0].timestamp;
        assert_eq!(timestamp.timestamp(), 1587427200);
    }
}