conditions/weather/
weather_api.rs

1use std::convert::From;
2
3use serde::Deserialize;
4use thiserror::Error;
5
6use super::CurrentConditions;
7use crate::{config::Config, icons::TimeOfDay, location::Location};
8
9pub struct Client {
10    is_valid: bool,
11    query: Vec<(String, String)>,
12}
13
14impl Client {
15    pub fn new(config: &Config, location: &Location) -> Self {
16        let key = config.weatherapi_token.clone();
17        let is_valid = key.is_some();
18
19        Self {
20            is_valid,
21            query: vec![
22                ("key".to_string(), key.unwrap_or_default()),
23                ("q".to_string(), location.loc.clone()),
24            ],
25        }
26    }
27}
28
29impl crate::api::Fetchable<Response, CurrentConditions> for Client {
30    fn url(&self) -> &'static str {
31        "http://api.weatherapi.com/v1/current.json"
32    }
33
34    fn is_valid(&self) -> bool {
35        self.is_valid
36    }
37
38    fn query(&self) -> Option<&Vec<(String, String)>> {
39        Some(&self.query)
40    }
41}
42
43#[derive(Error, Debug, PartialEq)]
44pub enum FetchConditionsError {
45    #[error("unknown error fetching weather conditions")]
46    Unknown,
47}
48
49impl std::convert::From<ureq::Error> for FetchConditionsError {
50    fn from(_: ureq::Error) -> Self {
51        Self::Unknown
52    }
53}
54
55impl std::convert::From<std::io::Error> for FetchConditionsError {
56    fn from(_: std::io::Error) -> Self {
57        Self::Unknown
58    }
59}
60
61/// Response represented as JSON response
62/// ```json
63/// {
64///   "current": {
65///     "condition": {
66///       "code": i32
67///     },
68///     "temp_f": f32,
69///     "is_day": u8
70///   }
71/// }
72/// ```
73#[derive(Debug, Deserialize)]
74pub struct Response {
75    current: WeatherAPIResultCurrent,
76}
77
78#[derive(Debug, Deserialize)]
79struct WeatherAPIResultCurrent {
80    condition: WeatherAPIResultCondition,
81    temp_c: f32,
82    temp_f: f32,
83    is_day: u8,
84}
85
86#[derive(Debug, Deserialize)]
87struct WeatherAPIResultCondition {
88    code: i32,
89}
90
91impl From<Response> for CurrentConditions {
92    fn from(result: Response) -> Self {
93        let icon = TimeOfDay::from(result.current.is_day)
94            .icon(&super::Source::WeatherAPI, result.current.condition.code);
95
96        Self {
97            temp_c: result.current.temp_c,
98            temp_f: result.current.temp_f,
99            icon,
100        }
101    }
102}
103
104#[cfg(test)]
105mod test {
106    use super::*;
107
108    #[test]
109    fn it_creates_client_with_query() {
110        let config = Config {
111            weatherapi_token: Some("token123".to_string()),
112            ..Default::default()
113        };
114        let location = Location {
115            loc: "loc_string".to_string(),
116            ..Default::default()
117        };
118        let client = Client::new(&config, &location);
119
120        assert_eq!(
121            client.query,
122            vec![
123                ("key".to_string(), "token123".to_string()),
124                ("q".to_string(), "loc_string".to_string())
125            ]
126        );
127    }
128
129    #[test]
130    fn test_weatherapi_from() {
131        let response = Response {
132            current: WeatherAPIResultCurrent {
133                condition: WeatherAPIResultCondition { code: 1087 },
134                temp_c: 10.0,
135                temp_f: 50.0,
136                is_day: 0,
137            },
138        };
139        let conditions = CurrentConditions::from(response);
140        assert!((conditions.temp_c - 10.0).abs() < f32::EPSILON);
141        assert!((conditions.temp_f - 50.0).abs() < f32::EPSILON);
142        assert_eq!(conditions.icon, "".to_string());
143    }
144}