eskom_se_push_api/
reqwest_blocking_client.rs

1//! A blocking client using the `reqwest` http client.
2//!
3//! # Optional
4//! Requires the `reqwest` and `sync` features to be enabled
5
6use http::header;
7use serde::de::DeserializeOwned;
8
9use crate::{
10  allowance::{AllowanceCheck, AllowanceCheckURL},
11  area_info::{AreaInfo, AreaInfoURLBuilder},
12  area_nearby::{AreaNearby, AreasNearbyURLBuilder},
13  area_search::{AreaSearch, AreaSearchURLBuilder},
14  constants::TOKEN_KEY,
15  errors::HttpError,
16  get_token_from_env,
17  status::{EskomStatus, EskomStatusUrl},
18  topics_nearby::{TopicsNearby, TopicsNearbyUrlBuilder},
19  Endpoint,
20};
21
22pub struct ReqwestBlockingCLient {
23  client: reqwest::blocking::Client,
24}
25
26impl ReqwestBlockingCLient {
27  /// Create new client using the `reqwest::blocking` Http client
28  /// `token` is the Eskom API token
29  pub fn new(token: String) -> Self {
30    let mut headers = header::HeaderMap::new();
31    headers.insert(
32      TOKEN_KEY,
33      header::HeaderValue::from_str(token.as_str()).unwrap(),
34    );
35
36    ReqwestBlockingCLient {
37      client: reqwest::blocking::ClientBuilder::new()
38        .default_headers(headers)
39        .build()
40        .unwrap(),
41    }
42  }
43
44  /// Creates new instance of Eskom API using token as a env variable.
45  /// Uses the [dotenv](https://crates.io/crates/dotenv) crate so it will load .env files if available.
46  /// `Note`: The default variable name is `ESKOMSEPUSH_API_KEY` if var_name is set to `None`.
47  /// `Note`: It will panic the env variable doesn't exist.
48  pub fn new_with_env(var_name: Option<&str>) -> Self {
49    match get_token_from_env(var_name) {
50      Ok(val) => {
51        let mut headers = header::HeaderMap::new();
52        headers.insert(
53          TOKEN_KEY,
54          header::HeaderValue::from_str(val.as_str()).unwrap(),
55        );
56
57        ReqwestBlockingCLient {
58          client: reqwest::blocking::ClientBuilder::new()
59            .default_headers(headers)
60            .build()
61            .unwrap(),
62        }
63      }
64      Err(e) => panic!("Error: {}", e),
65    }
66  }
67
68  /// The current and next loadshedding statuses for South Africa and (Optional) municipal overrides
69  /// `eskom` is the National status
70  /// Other keys in the `status` refer to different municipalities and potential overrides from the National status; most typically present is the key for `capetown`
71  pub fn get_load_shedding_status(&self) -> Result<EskomStatus, HttpError> {
72    let c = EskomStatusUrl::default();
73    c.reqwest_client(&self.client)
74  }
75
76  /// Obtain the `area_id` from Area Find or Area Search and use with this request. This single request has everything you need to monitor upcoming loadshedding events for the chosen suburb.
77  pub fn get_area_info(&self, area_id: &str) -> Result<AreaInfo, HttpError> {
78    let t = AreaInfoURLBuilder::default()
79      .area_id(area_id.to_owned())
80      .build()
81      .map_err(|_| HttpError::AreaIdNotSet)?;
82    t.reqwest_client(&self.client)
83  }
84
85  /// Find areas based on GPS coordinates (latitude and longitude).
86  /// The first area returned is typically the best choice for the coordinates - as it's closest to the GPS coordinates provided. However it could be that you are in the second or third area.
87  pub fn areas_nearby(&self, lat: f32, long: f32) -> Result<AreaNearby, HttpError> {
88    let t = AreasNearbyURLBuilder::default()
89      .latitude(lat)
90      .longitude(long)
91      .build()
92      .map_err(|_| HttpError::LongitudeOrLatitudeNotSet {
93        longitude: lat,
94        latitude: long,
95      })?;
96    t.reqwest_client(&self.client)
97  }
98
99  /// Search area based on text
100  pub fn areas_search(&self, search_term: &str) -> Result<AreaSearch, HttpError> {
101    let t = AreaSearchURLBuilder::default()
102      .search_term(search_term)
103      .build()
104      .map_err(|_| HttpError::SearchTextNotSet)?;
105    t.reqwest_client(&self.client)
106  }
107
108  /// Find topics created by users based on GPS coordinates (latitude and longitude). Can use this to detect if there is a potential outage/problem nearby
109  pub fn topics_nearby(&self, lat: f32, long: f32) -> Result<TopicsNearby, HttpError> {
110    let t = TopicsNearbyUrlBuilder::default()
111      .latitude(lat)
112      .longitude(long)
113      .build()
114      .map_err(|_| HttpError::LongitudeOrLatitudeNotSet {
115        longitude: lat,
116        latitude: long,
117      })?;
118    t.reqwest_client(&self.client)
119  }
120
121  /// Check allowance allocated for token
122  /// `NOTE`: This call doesn't count towards your quota.
123  pub fn check_allowance(&self) -> Result<AllowanceCheck, HttpError> {
124    let t = AllowanceCheckURL::default();
125    t.reqwest_client(&self.client)
126  }
127}
128
129/// A response handler for `reqwest::blocking` to map the response to the given structure or relevant error
130/// ```rust
131/// let statusUrl = EskomStatusUrlBuilder::default().build().unwrap();
132///
133/// let mut headers = header::HeaderMap::new();
134/// headers.insert(TOKEN_KEY, header::HeaderValue::from_str(token).unwrap());
135/// let client = reqwest::blocking::ClientBuilder::new().
136///   default_headers(headers).build().unwrap();
137///
138/// let api_response = client.get(url_endpoint.as_str()).send();
139/// let response = handle_ureq_response::<EskomStatus>(api_response);
140/// ```
141/// `response` is the ureq API response
142/// NOTE
143/// Requires the `reqwest` and `sync` features to be enabled
144pub fn handle_reqwest_response_blocking<T: DeserializeOwned>(
145  response: Result<reqwest::blocking::Response, reqwest::Error>,
146) -> Result<T, HttpError> {
147  use http::StatusCode;
148
149  use crate::errors::APIError;
150
151  match response {
152    Ok(resp) => {
153      let status_code = resp.status();
154      if status_code.is_server_error() {
155        Err(HttpError::ResponseError(
156          resp.error_for_status().unwrap_err(),
157        ))
158      } else {
159        match status_code {
160          StatusCode::BAD_REQUEST => Err(HttpError::APIError(APIError::BadRequest)),
161          StatusCode::FORBIDDEN => Err(HttpError::APIError(APIError::Forbidden)),
162          StatusCode::NOT_FOUND => Err(HttpError::APIError(APIError::NotFound)),
163          StatusCode::TOO_MANY_REQUESTS => Err(HttpError::APIError(APIError::TooManyRequests)),
164          _ => {
165            let r = resp.json::<T>();
166            match r {
167              Ok(r) => Ok(r),
168              Err(e) => {
169                if e.is_decode() {
170                  Err(HttpError::ResponseError(e))
171                } else {
172                  Err(HttpError::Unknown)
173                }
174              }
175            }
176          }
177        }
178      }
179    }
180    Err(err) => {
181      if err.is_timeout() {
182        Err(HttpError::Timeout)
183      } else if err.is_status() {
184        Err(HttpError::ResponseError(err))
185      } else {
186        Err(HttpError::NoInternet)
187      }
188    }
189  }
190}