diff --git a/.gitignore b/.gitignore index 088ba6b..ba4b577 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +.env + +# Vim backup files +**/*.swp diff --git a/Cargo.toml b/Cargo.toml index af7b038..5c275a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openweather" -version = "0.0.1" +version = "0.1.0" authors = ["Broderick Carlin "] description = "A rust crate wrapping openweather's API into a simple easy to use interface" repository = "https://github.com/BroderickCarlin/openweather" @@ -9,15 +9,25 @@ keywords = ["openweather", "openweathermaps", "weather", "api"] categories = ["api-bindings", "science"] license = "MIT" license-file = "LICENSE-MIT" +edition = "2018" [lib] name = "openweather" path = "src/lib.rs" [dependencies] -reqwest = "0.8.6" +log = "0.4.8" + +http_req = {version = "0.5.3", default-features = false, features = ["rust-tls"]} + serde_json = "1.0" -serde = "1.0.70" -serde_derive = "1.0.70" -time = "0.1.40" -url = "1.5.1" \ No newline at end of file +serde = "1.0.101" +serde_derive = "1.0.101" + +time = "0.1.42" +url = "2.1.0" +thiserror = "1.0.13" + + +[dev-dependencies] +dotenv = "0.15.0" diff --git a/examples/right_now.rs b/examples/right_now.rs index 4f50f8c..226e378 100644 --- a/examples/right_now.rs +++ b/examples/right_now.rs @@ -1,11 +1,16 @@ -extern crate openweather; - -use openweather::LocationSpecifier; +use openweather::{LocationSpecifier, Settings}; static API_KEY: &str = "YOUR_API_KEY_HERE"; -fn main() -{ - let loc = LocationSpecifier::CityAndCountryName{city:"Minneapolis", country:"USA"}; - let weather = openweather::get_current_weather(loc, API_KEY).unwrap(); +fn main() -> Result<(), openweather::Error> { + let loc = LocationSpecifier::CityAndCountryName { + city: "Minneapolis".to_string(), + country: "USA".to_string(), + }; + match openweather::get_current_weather(&loc, API_KEY, &Settings::default()) { + Ok(weather) => println!("Right now in Minneapolis, MN it is {}K", weather.main.temp), + Err(e) => println!("Error getting weather: {}", e), + } + let weather = openweather::get_current_weather(&loc, API_KEY, &Settings::default())?; println!("Right now in Minneapolis, MN it is {}K", weather.main.temp); -} \ No newline at end of file + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 882dc67..1e663f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,205 +1,310 @@ -extern crate serde; -extern crate serde_json; -#[macro_use] -extern crate serde_derive; -extern crate reqwest; -extern crate time; -extern crate url; +#![forbid(unsafe_code)] +use serde; +use serde_json; + +use time; +use url; + +mod location; +mod parameters; mod weather_types; -use weather_types::*; +pub use location::LocationSpecifier; +pub use parameters::{Language, Settings, Unit}; + +use log::debug; use url::Url; +pub use weather_types::*; static API_BASE: &str = "https://api.openweathermap.org/data/2.5/"; -#[derive(Debug)] -pub enum LocationSpecifier<'a>{ - CityAndCountryName {city: &'a str, country: &'a str}, - CityId(&'a str), - Coordinates {lat: f32, lon: f32}, - ZipCode {zip: &'a str, country: &'a str}, - - // The following location specifiers are used to specify multiple cities or a region - BoundingBox {lon_left: f32, lat_bottom: f32, lon_right: f32, lat_top: f32, zoom: f32}, - Circle {lat: f32, lon: f32, count: u16}, - CityIds(Vec<&'a str>), +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Openweather API error: {0}")] + Api(ErrorReport), + #[error("Error parsing to json: {0}")] + Parsing(#[from] serde_json::Error), + #[error("Error parsing to json. Parsing as Weather: {0} - Parsing as ErrorReport: {1}")] + Parsing2(serde_json::Error, serde_json::Error), + #[error("Http-Req error: {0}")] + Connection(#[from] http_req::error::Error), + #[error("Bad input: {msg}")] + Input { msg: String }, + #[error("Error parsing url: {0}")] + UrlParsing(#[from] url::ParseError), } -impl<'a> LocationSpecifier<'a> { - pub fn format(&'a self) -> Vec<(String, String)> { - match &self { - LocationSpecifier::CityAndCountryName {city, country} => { - if *country == "" { - return vec![("q".to_string(), city.to_string())]; - } else { - return vec![("q".to_string(), format!("{},{}", city, country))]; - } - } - LocationSpecifier::CityId(id) => { - return vec![("id".to_string(), id.to_string())]; - } - LocationSpecifier::Coordinates {lat, lon} => { - return vec![("lat".to_string(), format!("{}",lat)), - ("lon".to_string(), format!("{}",lon))]; - } - LocationSpecifier::ZipCode {zip, country} => { - if *country == "" { - return vec![("zip".to_string(), zip.to_string())]; - } else { - return vec![("zip".to_string(), format!("{},{}", zip, country))]; - } - } - LocationSpecifier::BoundingBox {lon_left, lat_bottom, lon_right, lat_top, zoom} => { - return vec![("bbox".to_string(), format!("{},{},{},{},{}", lon_left, lat_bottom, lon_right, lat_top, zoom))]; - } - LocationSpecifier::Circle {lat, lon, count} => { - return vec![("lat".to_string(), format!("{}", lat)), - ("lon".to_string(), format!("{}", lon)), - ("cnt".to_string(), format!("{}", count))]; - } - LocationSpecifier::CityIds(ids) => { - let mut locations: String = "".to_string(); - for loc in ids { locations += loc; } - return vec![("id".to_string(), locations.to_string())]; - } +/// A specialized Result type for prometheus. +pub type Result = core::result::Result; + +fn get(url: &str) -> Result +where + T: serde::de::DeserializeOwned, +{ + let mut res = Vec::new(); + + let status = http_req::request::get(url, &mut res)?; + debug!("Url: {:?}", url); + debug!("Status: {:?}", status); + debug!("Body_utf8: {:?}", res); + let res = String::from_utf8_lossy(&res); + debug!("Body_String: {:?}", res); + // fn err_report(res: &str) -> Result<()> { + // let res: ErrorReport = serde_json::from_str(&res).map(?; + // Ok(()) + // } + match serde_json::from_str(&res) { + Ok(val) => Ok(val), + Err(e_weather) => { + let err_report: ErrorReport = serde_json::from_str(&res) + .map_err(|e_report| Error::Parsing2(e_report, e_weather))?; + Err(Error::Api(err_report)) } } + // let res = serde_json::from_str(&res).map_err(|_| { + // let res: ErrorReport = serde_json::from_str(&res)?; + // Err(Error::Report(res)) + // })?; + // res + // serde.json + // let data: T = match serde_json::from_str(&res).map_err(|e| ? { + // Ok(val) => val, + // Err(_) => { + // let err_report: ErrorReport = match serde_json::from_str(&res) { + // Ok(report) => report, + // Err(e) => { + // return Err(ErrorReport { + // cod: 0, + // message: format!( + // "Got unexpected response: {:?} from (parsing) error: {:?}", + // res, e + // ), + // })?; + // } + // }; + // return Err(err_report); + // } + // }; + // Ok(data) } -fn get(url: &str) -> Result where T: serde::de::DeserializeOwned { - let res = reqwest::get(url).unwrap().text().unwrap(); - let data: T = match serde_json::from_str(&res) { - Ok(val) => {val}, - Err(_) => { - let err_report: ErrorReport = match serde_json::from_str(res.as_str()) { - Ok(report) => {report}, - Err(_) => { - return Err(ErrorReport{cod: 0, message: format!("Got unexpected response: {:?}", res)}); - } - }; - return Err(err_report); - } - }; - Ok(data) -} - -pub fn get_current_weather(location: LocationSpecifier, key: &str) -> Result { - let mut base = String::from(API_BASE); - let mut params = location.format(); +pub fn get_current_weather( + location: &LocationSpecifier, + key: &str, + settings: &Settings, +) -> Result { + let mut base = String::from(API_BASE); + let mut params = location.format(); - base.push_str("weather"); - params.push(("APPID".to_string(), key.to_string())); + base.push_str("weather"); + params.push(("APPID".to_string(), key.to_string())); + params.append(&mut settings.format()); - let url = Url::parse_with_params(&base, params).unwrap(); + let url = Url::parse_with_params(&base, params)?; get(&url.as_str()) } -pub fn get_5_day_forecast(location: LocationSpecifier, key: &str) -> Result { +pub fn get_5_day_forecast( + location: &LocationSpecifier, + key: &str, + settings: &Settings, +) -> Result { let mut base = String::from(API_BASE); - let mut params = location.format(); + let mut params = location.format(); - base.push_str("forecast"); - params.push(("APPID".to_string(), key.to_string())); + base.push_str("forecast"); + params.push(("APPID".to_string(), key.to_string())); + params.append(&mut settings.format()); - let url = Url::parse_with_params(&base, params).unwrap(); + let url = Url::parse_with_params(&base, params)?; get(&url.as_str()) } -pub fn get_16_day_forecast(location: LocationSpecifier, key: &str, len: u8) -> Result { +pub fn get_16_day_forecast( + location: &LocationSpecifier, + key: &str, + len: u8, + settings: &Settings, +) -> Result { if len > 16 || len == 0 { - return Err(ErrorReport{cod: 0, message: format!("Only support 1 to 16 day forecasts but {:?} requested", len)}); + return Err(Error::Input { + msg: format!("Only support 1 to 16 day forecasts but {:?} requested", len), + }); } let mut base = String::from(API_BASE); - let mut params = location.format(); + let mut params = location.format(); - base.push_str("forecast/daily"); - params.push(("cnt".to_string(), format!("{}", len))); - params.push(("APPID".to_string(), key.to_string())); + base.push_str("forecast/daily"); + params.push(("cnt".to_string(), format!("{}", len))); + params.push(("APPID".to_string(), key.to_string())); + params.append(&mut settings.format()); - let url = Url::parse_with_params(&base, params).unwrap(); + let url = Url::parse_with_params(&base, params)?; get(&url.as_str()) } -pub fn get_historical_data(location: LocationSpecifier, key: &str, start: time::Timespec, end: time::Timespec) -> Result { +pub fn get_historical_data( + location: &LocationSpecifier, + key: &str, + start: time::Timespec, + end: time::Timespec, + settings: &Settings, +) -> Result { let mut base = String::from(API_BASE); - let mut params = location.format(); + let mut params = location.format(); - base.push_str("history/city"); - params.push(("type".to_string(), "hour".to_string())); - params.push(("start".to_string(), format!("{}", start.sec))); - params.push(("end".to_string(), format!("{}", end.sec))); - params.push(("APPID".to_string(), key.to_string())); + base.push_str("history/city"); + params.push(("type".to_string(), "hour".to_string())); + params.push(("start".to_string(), format!("{}", start.sec))); + params.push(("end".to_string(), format!("{}", end.sec))); + params.push(("APPID".to_string(), key.to_string())); + params.append(&mut settings.format()); - let url = Url::parse_with_params(&base, params).unwrap(); + let url = Url::parse_with_params(&base, params)?; get(&url.as_str()) } -pub fn get_accumulated_temperature_data(location: LocationSpecifier, key: &str, start: time::Timespec, end: time::Timespec, threshold: u32) -> Result { +pub fn get_accumulated_temperature_data( + location: &LocationSpecifier, + key: &str, + start: time::Timespec, + end: time::Timespec, + threshold: u32, + settings: &Settings, +) -> Result { let mut base = String::from(API_BASE); - let mut params = location.format(); + let mut params = location.format(); - base.push_str("history/accumulated_temperature"); - params.push(("type".to_string(), "hour".to_string())); - params.push(("start".to_string(), format!("{}", start.sec))); - params.push(("end".to_string(), format!("{}", end.sec))); - params.push(("threshold".to_string(), format!("{}", threshold))); - params.push(("APPID".to_string(), key.to_string())); + base.push_str("history/accumulated_temperature"); + params.push(("type".to_string(), "hour".to_string())); + params.push(("start".to_string(), format!("{}", start.sec))); + params.push(("end".to_string(), format!("{}", end.sec))); + params.push(("threshold".to_string(), format!("{}", threshold))); + params.push(("APPID".to_string(), key.to_string())); + params.append(&mut settings.format()); - let url = Url::parse_with_params(&base, params).unwrap(); + let url = Url::parse_with_params(&base, params)?; get(&url.as_str()) } -pub fn get_accumulated_precipitation_data(location: LocationSpecifier, key: &str, start: time::Timespec, end: time::Timespec, threshold: u32) -> Result { +pub fn get_accumulated_precipitation_data( + location: &LocationSpecifier, + key: &str, + start: time::Timespec, + end: time::Timespec, + threshold: u32, + settings: &Settings, +) -> Result { let mut base = String::from(API_BASE); - let mut params = location.format(); + let mut params = location.format(); - base.push_str("history/accumulated_precipitation"); - params.push(("type".to_string(), "hour".to_string())); - params.push(("start".to_string(), format!("{}", start.sec))); - params.push(("end".to_string(), format!("{}", end.sec))); - params.push(("threshold".to_string(), format!("{}", threshold))); - params.push(("APPID".to_string(), key.to_string())); + base.push_str("history/accumulated_precipitation"); + params.push(("type".to_string(), "hour".to_string())); + params.push(("start".to_string(), format!("{}", start.sec))); + params.push(("end".to_string(), format!("{}", end.sec))); + params.push(("threshold".to_string(), format!("{}", threshold))); + params.push(("APPID".to_string(), key.to_string())); + params.append(&mut settings.format()); - let url = Url::parse_with_params(&base, params).unwrap(); + let url = Url::parse_with_params(&base, params)?; get(&url.as_str()) } -pub fn get_current_uv_index(location: LocationSpecifier, key: &str) -> Result { +pub fn get_current_uv_index( + location: &LocationSpecifier, + key: &str, + settings: &Settings, +) -> Result { let mut base = String::from(API_BASE); - let mut params = location.format(); + let mut params = location.format(); - base.push_str("uvi"); - params.push(("APPID".to_string(), key.to_string())); + base.push_str("uvi"); + params.push(("APPID".to_string(), key.to_string())); + params.append(&mut settings.format()); - let url = Url::parse_with_params(&base, params).unwrap(); + let url = Url::parse_with_params(&base, params)?; get(&url.as_str()) } -pub fn get_forecast_uv_index(location: LocationSpecifier, key: &str, len: u8) -> Result { +pub fn get_forecast_uv_index( + location: &LocationSpecifier, + key: &str, + len: u8, + settings: &Settings, +) -> Result { if len > 8 || len == 0 { - return Err(ErrorReport{cod: 0, message: format!("Only support 1 to 8 day forecasts but {:?} requested", len)}); + return Err(Error::Input { + msg: format!("Only support 1 to 8 day forecasts but {:?} requested", len), + }); } let mut base = String::from(API_BASE); - let mut params = location.format(); + let mut params = location.format(); - base.push_str("uvi/forecast"); - params.push(("cnt".to_string(), format!("{}", len))); - params.push(("APPID".to_string(), key.to_string())); + base.push_str("uvi/forecast"); + params.push(("cnt".to_string(), format!("{}", len))); + params.push(("APPID".to_string(), key.to_string())); + params.append(&mut settings.format()); - let url = Url::parse_with_params(&base, params).unwrap(); + let url = Url::parse_with_params(&base, params)?; get(&url.as_str()) } -pub fn get_historical_uv_index(location: LocationSpecifier, key: &str, start: time::Timespec, end: time::Timespec) -> Result { +pub fn get_historical_uv_index( + location: &LocationSpecifier, + key: &str, + start: time::Timespec, + end: time::Timespec, + settings: &Settings, +) -> Result { let mut base = String::from(API_BASE); - let mut params = location.format(); + let mut params = location.format(); - base.push_str("uvi/history"); - params.push(("start".to_string(), format!("{}", start.sec))); - params.push(("end".to_string(), format!("{}", end.sec))); - params.push(("APPID".to_string(), key.to_string())); + base.push_str("uvi/history"); + params.push(("start".to_string(), format!("{}", start.sec))); + params.push(("end".to_string(), format!("{}", end.sec))); + params.push(("APPID".to_string(), key.to_string())); + params.append(&mut settings.format()); - let url = Url::parse_with_params(&base, params).unwrap(); + let url = Url::parse_with_params(&base, params)?; get(&url.as_str()) -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use crate::{LocationSpecifier, Settings}; + static SETTINGS: &Settings = &Settings { + unit: None, + lang: None, + }; + + use dotenv; + fn api_key() -> String { + let key = "API_KEY"; + dotenv::var(key).expect("get api key for testing from .env file") + } + + #[test] + fn get_current_weather() { + let loc = LocationSpecifier::CityAndCountryName { + city: "Minneapolis".into(), + country: "USA".into(), + }; + let weather = crate::get_current_weather(&loc, &api_key(), SETTINGS) + .expect("failure getting current weather"); + println!("Right now in Minneapolis, MN it is {}C", weather.main.temp); + } + + #[test] + fn get_5_day_forecast() { + let loc = LocationSpecifier::CityAndCountryName { + city: "Minneapolis".into(), + country: "USA".into(), + }; + let weather = crate::get_5_day_forecast(&loc, &api_key(), SETTINGS) + .expect("failure getting 5 day forecast"); + println!("5 Day Report in Minneapolis, MN it is {:?}", weather.list); + } +} diff --git a/src/location.rs b/src/location.rs new file mode 100644 index 0000000..d532417 --- /dev/null +++ b/src/location.rs @@ -0,0 +1,90 @@ +#[derive(Debug)] +pub enum LocationSpecifier { + CityAndCountryName { + city: String, + country: String, + }, + CityId(String), + Coordinates { + lat: f32, + lon: f32, + }, + ZipCode { + zip: String, + country: String, + }, + + // The following location specifiers are used to specify multiple cities or a region + BoundingBox { + lon_left: f32, + lat_bottom: f32, + lon_right: f32, + lat_top: f32, + zoom: f32, + }, + Circle { + lat: f32, + lon: f32, + count: u16, + }, + CityIds(Vec), +} + +impl LocationSpecifier { + pub fn format(&self) -> Vec<(String, String)> { + match &self { + LocationSpecifier::CityAndCountryName { city, country } => { + if *country == "" { + return vec![("q".to_string(), city.to_string())]; + } else { + return vec![("q".to_string(), format!("{},{}", city, country))]; + } + } + LocationSpecifier::CityId(id) => { + return vec![("id".to_string(), id.to_string())]; + } + LocationSpecifier::Coordinates { lat, lon } => { + return vec![ + ("lat".to_string(), format!("{}", lat)), + ("lon".to_string(), format!("{}", lon)), + ]; + } + LocationSpecifier::ZipCode { zip, country } => { + if *country == "" { + return vec![("zip".to_string(), zip.to_string())]; + } else { + return vec![("zip".to_string(), format!("{},{}", zip, country))]; + } + } + LocationSpecifier::BoundingBox { + lon_left, + lat_bottom, + lon_right, + lat_top, + zoom, + } => { + return vec![( + "bbox".to_string(), + format!( + "{},{},{},{},{}", + lon_left, lat_bottom, lon_right, lat_top, zoom + ), + )]; + } + LocationSpecifier::Circle { lat, lon, count } => { + return vec![ + ("lat".to_string(), format!("{}", lat)), + ("lon".to_string(), format!("{}", lon)), + ("cnt".to_string(), format!("{}", count)), + ]; + } + LocationSpecifier::CityIds(ids) => { + let mut locations: String = "".to_string(); + for loc in ids { + locations += loc; + } + return vec![("id".to_string(), locations.to_string())]; + } + } + } +} diff --git a/src/parameters.rs b/src/parameters.rs new file mode 100644 index 0000000..efea6e2 --- /dev/null +++ b/src/parameters.rs @@ -0,0 +1,125 @@ +#[derive(Default, Debug)] +pub struct Settings { + pub unit: Option, + pub lang: Option, +} + +impl Settings { + pub fn format(&self) -> Vec<(String, String)> { + let mut res: Vec<(String, String)> = Vec::new(); + add_param(&mut res, self.unit); + add_param(&mut res, self.lang); + res + } +} + +fn add_param(settings: &mut Vec<(String, String)>, param: Option) { + param.map(|v: T| v.format().map(|u: (String, String)| settings.push(u))); +} + +trait FormatParameters { + fn format(&self) -> Option<(String, String)>; +} + +#[derive(Debug, Clone, Copy)] +pub enum Unit { + // Celcius, default + Metric, + // Kelvin + Standard, + // Fahrenheit + Imperial, +} + +impl FormatParameters for Unit { + fn format(&self) -> Option<(String, String)> { + match self { + Unit::Metric => Some(("units".to_string(), "metric".to_string())), + Unit::Standard => None, + Unit::Imperial => Some(("units".to_string(), "imperial".to_string())), + } + } +} + +/// Translation is only applied for the description field! +#[derive(Debug, Clone, Copy)] +pub enum Language { + Arabic, + Bulgarian, + Catalan, + Czech, + German, + Greek, + English, + PersianFarsi, + Finnish, + French, + Galician, + Croatian, + Hungarian, + Italian, + Japanese, + Korean, + Latvian, + Lithuanian, + Macedonian, + Dutch, + Polish, + Portuguese, + Romanian, + Russian, + Swedish, + Slovak, + Slovenian, + Spanish, + Turkish, + Ukrainian, + Vietnamese, + ChineseSimplified, + ChineseTraditional, +} + +impl FormatParameters for Language { + fn format(&self) -> Option<(String, String)> { + use Language::*; + Some(( + String::from("lang"), + match self { + Arabic => "ar", + Bulgarian => "bg", + Catalan => "ca", + Czech => "cz", + German => "de", + Greek => "el", + English => "en", + PersianFarsi => "fa", + Finnish => "fi", + French => "fr", + Galician => "gl", + Croatian => "hr", + Hungarian => "hu", + Italian => "it", + Japanese => "ja", + Korean => "kr", + Latvian => "la", + Lithuanian => "lt", + Macedonian => "mk", + Dutch => "nl", + Polish => "pl", + Portuguese => "pt", + Romanian => "ro", + Russian => "ru", + Swedish => "se", + Slovak => "sk", + Slovenian => "sl", + Spanish => "es", + Turkish => "tr", + Ukrainian => "ua", + Vietnamese => "vi", + ChineseSimplified => "zh_cn", + ChineseTraditional => "zh_tw", + } + .to_string(), + )) + } +} diff --git a/src/weather_types.rs b/src/weather_types.rs index 5eba86e..31228b9 100644 --- a/src/weather_types.rs +++ b/src/weather_types.rs @@ -1,18 +1,24 @@ -#[derive(Serialize, Deserialize, Debug)] +use serde_derive::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct Coordinates { pub lat: f32, pub lon: f32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct CityShort { pub id: u32, pub name: String, pub coord: Coordinates, pub country: String, + pub population: u32, + pub timezone: i32, + pub sunrise: u64, + pub sunset: u64, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct CityLong { pub geoname_id: u64, pub name: String, @@ -20,24 +26,27 @@ pub struct CityLong { pub lon: u32, pub country: String, pub iso2: String, -#[serde(rename="type")] + #[serde(rename = "type")] pub city_type: String, pub population: u32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct Main { pub temp: f32, pub temp_min: f32, pub temp_max: f32, pub pressure: f32, + #[serde(default)] pub sea_level: Option, + #[serde(default)] pub grnd_level: Option, pub humidity: f32, - pub temp_kf: Option + #[serde(default)] + pub temp_kf: Option, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct Weather { pub id: u32, pub main: String, @@ -45,30 +54,38 @@ pub struct Weather { pub icon: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct Clouds { pub all: i32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct Wind { pub speed: f32, - pub deg: f32, + pub deg: Option, pub gust: Option, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct Rain { -#[serde(rename="3h")] + /// Rain volume in mm + #[serde(rename = "3h")] + pub three_h: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] +pub struct Snow { + /// Snow volume + #[serde(rename = "3h")] pub three_h: Option, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct System { - pub pod: String + pub pod: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct TempDaily { pub day: f32, pub min: f32, @@ -78,19 +95,20 @@ pub struct TempDaily { pub morn: f32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct TimeSliceHourly { pub dt: u64, pub main: Main, pub weather: Vec, pub clouds: Clouds, pub wind: Wind, - pub rain: Rain, + pub rain: Option, + pub snow: Option, pub sys: System, - pub dt_txt: String + pub dt_txt: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct TimeSliceDaily { pub dt: u64, pub temp: TempDaily, @@ -102,48 +120,60 @@ pub struct TimeSliceDaily { pub clouds: i32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct ErrorReport { - pub cod: u32, + pub cod: u32, pub message: String, } -#[derive(Serialize, Deserialize, Debug)] +use core::fmt; +impl fmt::Display for ErrorReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Code: {}, Message: {}", self.cod, self.message) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct Sys { -#[serde(rename="type")] + #[serde(rename = "type")] pub message_type: u32, pub id: u32, - pub message: f32, + pub message: Option, pub country: String, pub sunrise: u64, - pub sunset: u64, + pub sunset: u64, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct WeatherReportCurrent { pub coord: Coordinates, pub weather: Vec, pub base: String, pub main: Main, - pub visibility: u32, + pub visibility: Option, pub wind: Wind, + pub rain: Option, + pub snow: Option, pub clouds: Clouds, pub dt: u64, pub sys: Sys, + pub timezone: Option, pub id: u64, pub name: String, + pub message: Option, pub cod: u16, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct WeatherReport5Day { pub cod: String, - pub message: f32, + pub message: Option, + pub cnt: u8, pub list: Vec, pub city: CityShort, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct WeatherReport16Day { pub cod: String, pub message: f32, @@ -152,7 +182,7 @@ pub struct WeatherReport16Day { pub list: Vec, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct WeatherReportHistoricalElement { pub main: Main, pub wind: Wind, @@ -161,7 +191,7 @@ pub struct WeatherReportHistoricalElement { pub dt: u64, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct WeatherReportHistorical { pub message: String, pub cod: String, @@ -171,14 +201,14 @@ pub struct WeatherReportHistorical { pub list: Vec, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct WeatherAccumulatedTemperatureElement { pub date: String, pub temp: f32, pub count: u32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct WeatherAccumulatedPrecipitation { pub message: String, pub cod: String, @@ -187,14 +217,14 @@ pub struct WeatherAccumulatedPrecipitation { pub list: Vec, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct WeatherAccumulatedPrecipitationElement { pub date: String, pub rain: f32, pub count: u32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct WeatherAccumulatedTemperature { pub message: String, pub cod: String, @@ -203,7 +233,7 @@ pub struct WeatherAccumulatedTemperature { pub list: Vec, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct UvIndex { pub lat: f32, pub lon: f32, @@ -212,12 +242,12 @@ pub struct UvIndex { pub value: f32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct ForecastUvIndex { pub list: Vec, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct HistoricalUvIndexElement { pub lat: f32, pub lon: f32, @@ -226,7 +256,86 @@ pub struct HistoricalUvIndexElement { pub value: u32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] pub struct HistoricalUvIndex { pub list: Vec, } + +#[cfg(test)] +mod tests { + use crate::*; + + #[test] + /// This test/derive once failed + fn current_weather_derive_failure_1() { + let der_string = "{\"coord\":{\"lon\":10.45,\"lat\":49.57},\"weather\":[{\"id\":801,\"main\":\"Clouds\",\"description\":\"Ein paar Wolken\",\"icon\":\"02d\"}],\"base\":\"stations\",\"main\":{\"temp\":2.94,\"pressure\":998,\"humidity\":86,\"temp_min\":1.67,\"temp_max\":3.89},\"visibility\":10000,\"wind\":{\"speed\":1},\"clouds\":{\"all\":20},\"dt\":1573810268,\"sys\":{\"type\":1,\"id\":1274,\"country\":\"DE\",\"sunrise\":1573799471,\"sunset\":1573832742},\"timezone\":3600,\"id\":2820859,\"name\":\"Berlin\",\"cod\":200}"; + + let weather_report: WeatherReportCurrent = + serde_json::from_str(&der_string).expect("current weather derive failure testcase 1"); + + let weather_report_derived = WeatherReportCurrent { + coord: Coordinates { + lon: 10.45, + lat: 49.57, + }, + weather: vec![Weather { + id: 801, + main: "Clouds".to_string(), + description: "Ein paar Wolken".to_string(), + icon: "02d".to_string(), + }], + base: "stations".to_string(), + main: Main { + temp: 2.94, + pressure: 998.0, + humidity: 86.0, + temp_min: 1.67, + temp_max: 3.89, + ..Default::default() + }, + visibility: Some(10000), + wind: Wind { + speed: 1.0, + ..Default::default() + }, + clouds: Clouds { + all: 20, + ..Default::default() + }, + dt: 1573810268, + sys: Sys { + message_type: 1, + id: 1274, + country: "DE".to_string(), + sunrise: 1573799471, + sunset: 1573832742, + ..Default::default() + }, + timezone: Some(3600), + id: 2820859, + name: "Berlin".to_string(), + cod: 200, + + ..Default::default() + }; + + assert_eq!(weather_report.timezone, Some(3600)); + assert_eq!( + weather_report.clouds, + Clouds { + all: 20, + ..Default::default() + } + ); + + assert_eq!( + weather_report.wind, + Wind { + speed: 1.0, + ..Default::default() + } + ); + + assert_eq!(weather_report, weather_report_derived); + } +}