Skip to content

Commit

Permalink
Merge pull request #5 from lukaspustina/graceful_errors
Browse files Browse the repository at this point in the history
Add graceful API error handling
  • Loading branch information
lukaspustina committed Aug 31, 2020
2 parents 6979e63 + 8375de4 commit ad38096
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 43 deletions.
110 changes: 80 additions & 30 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
pub mod authenticate;
pub mod get_home_status;
pub mod get_homes_data;
pub mod get_measure;
pub mod get_station_data;
pub mod set_room_thermpoint;
use std::collections::HashMap;

use failure::Fail;
use reqwest::{Response, StatusCode};
use serde::de::DeserializeOwned;
use serde::Deserialize;

use crate::errors::{ErrorKind, Result};
use authenticate::{Scope, Token};
use get_home_status::HomeStatus;
use get_homes_data::HomesData;
use get_measure::Measure;
use get_station_data::StationData;

use failure::Fail;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use crate::errors::{Error, ErrorKind, Result};

pub mod authenticate;
pub mod get_home_status;
pub mod get_homes_data;
pub mod get_measure;
pub mod get_station_data;
pub mod set_room_thermpoint;

pub trait Netatmo {
fn get_home_status(&self, parameters: &get_home_status::Parameters) -> Result<HomeStatus>;
Expand Down Expand Up @@ -65,11 +69,11 @@ impl<'a> UnauthenticatedClient<'a> {
.map_err(|e| e.context(ErrorKind::AuthenticationFailed).into())
}

pub(crate) fn call<T>(&self, url: &str, params: &HashMap<&str, &str>) -> Result<T>
pub(crate) fn call<T>(&self, name: &'static str, url: &str, params: &HashMap<&str, &str>) -> Result<T>
where
T: DeserializeOwned,
{
api_call(&self.http, url, params)
api_call(name, &self.http, url, params)
}
}

Expand All @@ -83,61 +87,107 @@ impl AuthenticatedClient {
&self.token
}

pub(crate) fn call<'a, T>(&'a self, url: &str, params: &mut HashMap<&str, &'a str>) -> Result<T>
pub(crate) fn call<'a, T>(&'a self, name: &'static str, url: &str, params: &mut HashMap<&str, &'a str>) -> Result<T>
where
T: DeserializeOwned,
{
params.insert("access_token", &self.token.access_token);
api_call(&self.http, url, params)
api_call(name, &self.http, url, params)
}
}

fn api_call<T>(http: &reqwest::Client, url: &str, params: &HashMap<&str, &str>) -> Result<T>
fn api_call<T>(name: &'static str, http: &reqwest::Client, url: &str, params: &HashMap<&str, &str>) -> Result<T>
where
T: DeserializeOwned,
{
let mut res = http
.post(url)
.form(&params)
.send()
.map_err(|e| e.context(ErrorKind::FailedToSendRequest))?;
.map_err(|e| e.context(ErrorKind::FailedToSendRequest))?
.general_err_handler(name, StatusCode::OK)?;

let body = res.text().map_err(|e| e.context(ErrorKind::FailedToReadResponse))?;
serde_json::from_str::<T>(&body).map_err(|e| e.context(ErrorKind::JsonDeserializationFailed).into())
}

pub(crate) trait GeneralErrHandler {
type T: std::marker::Sized;

fn general_err_handler(self, name: &'static str, expected_status: StatusCode) -> Result<Self::T>;
}

#[derive(Debug, Deserialize)]
struct ApiError {
#[serde(rename = "error")]
details: ApiErrorDetails,
}

#[derive(Debug, Deserialize)]
struct ApiErrorDetails {
code: isize,
message: String,
}

impl GeneralErrHandler for Response {
type T = Response;

fn general_err_handler(mut self, name: &'static str, expected_status: StatusCode) -> Result<Self> {
match self.status() {
code if code == expected_status => Ok(self),
code @ StatusCode::BAD_REQUEST
| code @ StatusCode::UNAUTHORIZED
| code @ StatusCode::FORBIDDEN
| code @ StatusCode::NOT_FOUND
| code @ StatusCode::NOT_ACCEPTABLE
| code @ StatusCode::INTERNAL_SERVER_ERROR => {
let body = self.text().map_err(|e| {
e.context(ErrorKind::UnknownApiCallFailure {
name,
status_code: code.as_u16(),
})
})?;
let err: ApiError = serde_json::from_str(&body).map_err(|e| {
e.context(ErrorKind::UnknownApiCallFailure {
name,
status_code: code.as_u16(),
})
})?;
Err(Error::from(ErrorKind::ApiCallFailed {
name,
code: err.details.code,
msg: err.details.message,
}))
}
code => Err(Error::from(ErrorKind::UnknownApiCallFailure {
name,
status_code: code.as_u16(),
})),
}
}
}

impl Netatmo for AuthenticatedClient {
fn get_homes_data(&self, parameters: &get_homes_data::Parameters) -> Result<HomesData> {
get_homes_data::get_homes_data(&self, parameters)
.map_err(|e| e.context(ErrorKind::ApiCallFailed("get_homes_data".to_string())).into())
}

fn get_home_status(&self, parameters: &get_home_status::Parameters) -> Result<HomeStatus> {
get_home_status::get_home_status(&self, parameters).map_err(|e| {
e.context(ErrorKind::ApiCallFailed("get_home_status".to_string()))
.into()
})
get_home_status::get_home_status(&self, parameters)
}

fn get_station_data(&self, device_id: &str) -> Result<StationData> {
get_station_data::get_station_data(&self, device_id).map_err(|e| {
e.context(ErrorKind::ApiCallFailed("get_station_data".to_string()))
.into()
})
get_station_data::get_station_data(&self, device_id)
}

fn get_measure(&self, parameters: &get_measure::Parameters) -> Result<Measure> {
get_measure::get_measure(&self, parameters)
.map_err(|e| e.context(ErrorKind::ApiCallFailed("get_measure".to_string())).into())
}

fn set_room_thermpoint(
&self,
parameters: &set_room_thermpoint::Parameters,
) -> Result<set_room_thermpoint::Response> {
set_room_thermpoint::set_room_thermpoint(&self, parameters).map_err(|e| {
e.context(ErrorKind::ApiCallFailed("set_room_thermpoint".to_string()))
.into()
})
set_room_thermpoint::set_room_thermpoint(&self, parameters)
}
}
2 changes: 1 addition & 1 deletion src/client/authenticate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,5 @@ pub(crate) fn get_token(
params.insert("grant_type", "password");
params.insert("scope", &scopes_str);

unauthenticated_client.call("https://api.netatmo.com/oauth2/token", &params)
unauthenticated_client.call("oauth2/token", "https://api.netatmo.com/oauth2/token", &params)
}
2 changes: 1 addition & 1 deletion src/client/get_home_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,5 @@ impl<'a> From<&'a Parameters<'a>> for HashMap<&str, String> {
pub(crate) fn get_home_status(client: &AuthenticatedClient, parameters: &Parameters) -> Result<HomeStatus> {
let params: HashMap<&str, String> = parameters.into();
let mut params = params.iter().map(|(k, v)| (*k, v.as_ref())).collect();
client.call("https://api.netatmo.com/api/homestatus", &mut params)
client.call("get_home_status", "https://api.netatmo.com/api/homestatus", &mut params)
}
2 changes: 1 addition & 1 deletion src/client/get_homes_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,5 +186,5 @@ impl<'a> From<&'a Parameters<'a>> for HashMap<&str, String> {
pub(crate) fn get_homes_data(client: &AuthenticatedClient, parameters: &Parameters) -> Result<HomesData> {
let params: HashMap<&str, String> = parameters.into();
let mut params = params.iter().map(|(k, v)| (*k, v.as_ref())).collect();
client.call("https://api.netatmo.com/api/homesdata", &mut params)
client.call("get_homes_data", "https://api.netatmo.com/api/homesdata", &mut params)
}
2 changes: 1 addition & 1 deletion src/client/get_measure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ pub fn get_measure(client: &AuthenticatedClient, parameters: &Parameters) -> Res
let params: HashMap<&str, String> = parameters.into();
let mut params = params.iter().map(|(k, v)| (*k, v.as_ref())).collect();

client.call("https://api.netatmo.com/api/getmeasure", &mut params)
client.call("get_measure", "https://api.netatmo.com/api/getmeasure", &mut params)
}

fn de_body_values<'de, D>(deserializer: D) -> ::std::result::Result<HashMap<usize, Vec<Option<f64>>>, D::Error>
Expand Down
14 changes: 10 additions & 4 deletions src/client/get_station_data.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::{client::AuthenticatedClient, errors::Result};
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use crate::{client::AuthenticatedClient, errors::Result};

#[derive(Debug, Serialize, Deserialize)]
pub struct StationData {
Expand Down Expand Up @@ -110,14 +111,19 @@ pub(crate) fn get_station_data(client: &AuthenticatedClient, device_id: &str) ->
let mut params: HashMap<&str, &str> = HashMap::default();
params.insert("device_id", device_id);

client.call("https://api.netatmo.com/api/getstationsdata", &mut params)
client.call(
"get_station_data",
"https://api.netatmo.com/api/getstationsdata",
&mut params,
)
}

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

use super::*;

mod get_station_data {
use super::*;

Expand Down
6 changes: 5 additions & 1 deletion src/client/set_room_thermpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,9 @@ pub fn set_room_thermpoint(client: &AuthenticatedClient, parameters: &Parameters
let params: HashMap<&str, String> = parameters.into();
let mut params = params.iter().map(|(k, v)| (*k, v.as_ref())).collect();

client.call("https://api.netatmo.com/api/setroomthermpoint", &mut params)
client.call(
"set_room_thermpoint",
"https://api.netatmo.com/api/setroomthermpoint",
&mut params,
)
}
23 changes: 19 additions & 4 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use failure::{Backtrace, Context, Fail};
use std::fmt;

use failure::{Backtrace, Context, Fail};

/// The error kind for errors that get returned in the crate
#[derive(Eq, PartialEq, Debug, Fail)]
pub enum ErrorKind {
Expand All @@ -12,8 +13,17 @@ pub enum ErrorKind {
FailedToReadResponse,
#[fail(display = "failed to authenticate")]
AuthenticationFailed,
#[fail(display = "API call '{}' failed", _0)]
ApiCallFailed(String),
#[fail(display = "API call '{}' failed with code {} because {}", name, code, msg)]
ApiCallFailed {
name: &'static str,
code: isize,
msg: String,
},
#[fail(
display = "API call '{}' failed for unknown reason with status code {}",
name, status_code
)]
UnknownApiCallFailure { name: &'static str, status_code: u16 },
}

impl Clone for ErrorKind {
Expand All @@ -24,7 +34,12 @@ impl Clone for ErrorKind {
FailedToSendRequest => FailedToSendRequest,
FailedToReadResponse => FailedToReadResponse,
AuthenticationFailed => AuthenticationFailed,
ApiCallFailed(ref x) => ApiCallFailed(x.clone()),
ApiCallFailed { name, code, ref msg } => ApiCallFailed {
name,
code,
msg: msg.clone(),
},
UnknownApiCallFailure { name, status_code } => UnknownApiCallFailure { name, status_code },
}
}
}
Expand Down

0 comments on commit ad38096

Please sign in to comment.