1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
extern crate reqwest;
extern crate serde_json;

use futures::executor;
use http::StatusCode;
use regex::Regex;
use serde::Deserialize;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

#[cfg(test)]
mod tests;

/// Location coordinates
#[derive(Deserialize, Debug)]
pub struct Coord {
    /// geo location, longitude
    pub lon: f64,
    /// geo location, latitude
    pub lat: f64,
}

/// Weather condition description
#[derive(Deserialize, Debug)]
pub struct Weather {
    /// Weather condition id
    pub id: u64,
    /// Group of weather parameters (Rain, Snow, Extreme etc.)
    pub main: String,
    /// Weather condition
    pub description: String,
    /// Weather icon id
    pub icon: String,
}

/// Detailed weather report
#[derive(Deserialize, Debug)]
pub struct Main {
    /// Temperature. Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.
    pub temp: f64,
    /// Temperature. This temperature parameter accounts for the human perception of weather.
    /// Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.
    pub feels_like: f64,
    /// Atmospheric pressure (on the sea level, if there is no sea_level or grnd_level data), hPa
    pub pressure: f64,
    /// Humidity, %
    pub humidity: f64,
    /// Minimum temperature at the moment.
    /// This is minimal currently observed temperature (within large megalopolises and urban areas).
    /// Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.
    pub temp_min: f64,
    /// Maximum temperature at the moment.
    /// This is maximal currently observed temperature (within large megalopolises and urban areas).
    /// Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.
    pub temp_max: f64,
    /// Atmospheric pressure on the sea level, hPa
    pub sea_level: Option<f64>,
    /// Atmospheric pressure on the ground level, hPa
    pub grnd_level: Option<f64>,
}

/// Detailed wind report
#[derive(Deserialize, Debug)]
pub struct Wind {
    /// Wind speed. Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour.
    pub speed: f64,
    /// Wind direction, degrees (meteorological)
    pub deg: f64,
    /// Wind gust. Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour
    pub gust: Option<f64>,
}

/// Detailed clouds report
#[derive(Deserialize, Debug)]
pub struct Clouds {
    /// Cloudiness, %
    pub all: f64,
}

/// Rain or snow volume report
#[derive(Deserialize, Debug)]
pub struct Volume {
    /// Volume for the last 1 hour, mm
    #[serde(rename = "1h")]
    pub h1: Option<f64>,
    /// Volume for the last 3 hours, mm
    #[serde(rename = "3h")]
    pub h3: Option<f64>,
}

/// Additional information
#[derive(Deserialize, Debug)]
pub struct Sys {
    /// Internal parameter
    #[serde(rename = "type")]
    pub type_: Option<u64>,
    /// Internal parameter
    pub id: Option<u64>,
    /// Internal parameter
    pub message: Option<f64>,
    /// Country code (GB, JP etc.)
    pub country: String,
    /// Sunrise time, unix, UTC
    pub sunrise: u64,
    /// Sunset time, unix, UTC
    pub sunset: u64,
}

#[derive(Deserialize, Debug)]
/// current weather report in a nested struct
pub struct CurrentWeather {
    /// report origin coordinates
    pub coord: Coord,
    /// vector with one item of weather condition descriptions
    pub weather: Vec<Weather>,
    /// Internal parameter
    pub base: String,
    /// detailed weather report
    pub main: Main,
    /// Visibility, meter
    pub visibility: u64,
    /// detailed wind report
    pub wind: Wind,
    /// detailed clouds report
    pub clouds: Clouds,
    /// detailed rain report
    pub rain: Option<Volume>,
    /// detailed snow report
    pub snow: Option<Volume>,
    /// Time of data calculation, unix, UTC
    pub dt: i64,
    /// additional information
    pub sys: Sys,
    /// Shift in seconds from UTC
    pub timezone: i64,
    /// City ID
    pub id: u64,
    /// City name
    pub name: String,
    /// Internal parameter
    pub cod: u64,
}

/// Receiver object you get from `init()` and have top handle to `update()`.
pub type Receiver = mpsc::Receiver<Result<CurrentWeather, String>>;
/// Loading error messaage you get at the first call of `update()`.
pub const LOADING: &str = "loading...";

/// Spawns a thread which fetches the current weather from
/// [openweathermap.org](https://openweathermap.org) periodically.
/// #### Parameters
/// - `location`: Can be a city name, a city ID or a geographical coordinate:
///     - city name: may be followed by comma separated state code and/or country code (e.g. `"Berlin,DE"`).
///     - city ID: which can be found at [this](https://openweathermap.org/find) where you will get link that includes the ID
///         - e.g. `"2950159"` for Berlin, Germany
///     - coordinates: given by comma separated latitude and longitude (e.g. `"52.5244,13.4105"`). |
/// - `units`: One of the following:
///     - `"metric"`: meters, m/s, °C, etc.
///     - `"imperial"`: miles, mi/s, °F, etc.
///     - `"standard"`: meters, m/s, K, etc.
/// - `lang`: Language code:
///     - `"en"`: for English
///     - `"de"`: for German
///     - see [this list](https://openweathermap.org/current#multi) for all available language codes
/// - `api_key`: Your API key which you can get [here](https://openweathermap.org/price)
/// - `poll_mins`: Update interval:
///     - `> 0`: duration of poll period in minutes (`10` is recommended)
///     - `= 0`: thread will terminate after the first successful update.
/// #### Return value
/// - `openweathermap::Receiver`: Handle this to `openweathermap::update()` to get the latest weather update.
///
///    The return value is a `mpsc` *channel receiver*:
///    ```rust
///     pub type Receiver = std::sync::mpsc::Receiver<Result<openweathermap::CurrentWeather, String>>;
///    ```

pub fn init(location: &str, units: &str, lang: &str, api_key: &str, poll_mins: u64) -> Receiver {
    // generate correct request URL depending on city is id or name
    let url = match location.parse::<u64>().is_ok() {
        true => format!(
            "https://api.openweathermap.org/data/2.5/weather?id={}&units={}&lang={}&appid={}",
            location, units, lang, api_key
        ),
        false => {
            let re = Regex::new(r"(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)").unwrap();
            match re.captures(&location) {
                Some(caps) => format!("https://api.openweathermap.org/data/2.5/weather?lat={}&lon={}&units={}&lang={}&appid={}",
                            caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str(), units, lang, api_key ),
                None => format!(
                            "https://api.openweathermap.org/data/2.5/weather?q={}&units={}&lang={}&appid={}",
                            location, units, lang, api_key ),
            }
        }
    };
    // fork thread that continuously fetches weather updates every <poll_mins> minutes
    let period = Duration::from_secs(60 * poll_mins);
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        tx.send(Err(LOADING.to_string())).unwrap_or(());
        loop {
            match reqwest::blocking::get(&url) {
                Ok(response) => match response.status() {
                    StatusCode::OK => match serde_json::from_str(&response.text().unwrap()) {
                        Ok(w) => {
                            tx.send(Ok(w)).unwrap_or(());
                            if period == Duration::new(0, 0) {
                                break;
                            }
                            thread::sleep(period);
                        }
                        Err(e) => tx.send(Err(e.to_string())).unwrap_or(()),
                    },
                    _ => tx.send(Err(response.status().to_string())).unwrap_or(()),
                },
                Err(_e) => (),
            }
        }
    });
    // return receiver that provides the updated weather as json string
    return rx;
}

/// Get current weather update that the spawned thread could fetched.
/// #### Parameters
/// - `receiver`: the *channel receiver* from preceded call to `openweathermap::init()`
/// #### Returng value
/// - ⇒ `None`: No update available
/// - ⇒ `Some(Result)`: Update available
///     - ⇒ `Ok(CurrentWeather)`: Weather information in a nested struct called `CurrentWeather`
///         (see also [*OpenWeatherMap* documentation](https://openweathermap.org/current#parameter) for details)
///     - ⇒ `Err(String)`: Error message about any occured http or json issue
///         - e.g. `401 Unauthorized`: if your API key is invalid
///         - some json parser error message if response from OpenWeatherMap could not be parsed
pub fn update(receiver: &Receiver) -> Option<Result<CurrentWeather, String>> {
    match receiver.try_recv() {
        Ok(response) => Some(response),
        Err(_e) => None,
    }
}

/// Fetch current weather update once and stop thread immediately after success.
/// Returns the result in a *future*.
/// #### Parameters
/// - `location`: Can be a city name, a city ID or a geographical coordinate:
///     - city name: may be followed by comma separated state code and/or country code (e.g. `"Berlin,DE"`).
///     - city ID: which can be found at [this](https://openweathermap.org/find) where you will get link that includes the ID
///         - e.g. `"2950159"` for Berlin, Germany
///     - coordinates: given by comma separated latitude and longitude (e.g. `"52.5244,13.4105"`). |
/// - `units`: One of the following:
///     - `"metric"`: meters, m/s, °C, etc.
///     - `"imperial"`: miles, mi/s, °F, etc.
///     - `"standard"`: meters, m/s, K, etc.
/// - `lang`: Language code:
///     - `"en"`: for English
///     - `"de"`: for German
///     - see [this list](https://openweathermap.org/current#multi) for all available language codes
/// - `api_key`: Your API key which you can get [here](https://openweathermap.org/price)
/// #### Return value
/// - ⇒ `Ok(CurrentWeather)`: weather information in a nested struct called `CurrentWeather`
///     (see also [*OpenWeatherMap* documentation](https://openweathermap.org/current#parameter) for details)
/// - ⇒ `Err(String)`: Error message about any occured http or json issue
///         - e.g. `401 Unauthorized` if your API key is invalid
///         - some json parser error message if response from OpenWeatherMap could not be parsed
pub async fn weather(
    location: &str,
    units: &str,
    lang: &str,
    api_key: &str,
) -> Result<CurrentWeather, String> {
    let r = init(location, units, lang, api_key, 0);
    loop {
        match update(&r) {
            Some(response) => match response {
                Ok(current) => return Ok(current),
                Err(e) => {
                    if e != LOADING {
                        return Err(e);
                    }
                }
            },
            None => (),
        }
    }
}

/// synchronous functions
pub mod blocking {
    use super::*;
    /// Fetches a weather update once and stops the thread immediately after success then returns the update.
    /// #### Parameters
    /// - `location`: Can be a city name, a city ID or a geographical coordinate:
    ///     - city name may be followed by comma separated state code and/or country code (e.g. `"Berlin,DE"`).
    ///     - city ID which can be found at [this](https://openweathermap.org/find) where you will get link that includes the ID
    ///         - e.g. `"2950159"` for Berlin, Germany
    ///     - coordinates given by comma separated latitude and longitude (e.g. `"52.5244,13.4105"`). |
    /// - `units`: One of the following:
    ///     - `"metric"`: meters, m/s, °C, etc.
    ///     - `"imperial"`: miles, mi/s, °F, etc.
    ///     - `"standard"`: meters, m/s, K, etc.
    /// - `lang`: Language code:
    ///     - `"en"`: for English
    ///     - `"de"`: for German
    ///     - see [this list](https://openweathermap.org/current#multi) for all available language codes
    /// - `api_key`: Your API key which you can get [here](https://openweathermap.org/price)
    /// #### Return value
    /// - ⇒ `Ok(CurrentWeather)`: weather information in a nested struct called `CurrentWeather`
    ///     (see also [*OpenWeatherMap* documentation](https://openweathermap.org/current#parameter) for details)
    /// - ⇒ `Err(String)`: Error message about any occured http or json issue
    ///         - e.g. `401 Unauthorized` if your API key is invalid
    ///         - some json parser error message if response from OpenWeatherMap could not be parsed
    pub fn weather(
        location: &str,
        units: &str,
        lang: &str,
        api_key: &str,
    ) -> Result<CurrentWeather, String> {
        // wait for result
        executor::block_on(super::weather(location, units, lang, api_key))
    }
}