From 8375de43139dff61e2775d5a091c35cf32b229c4 Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Tue, 25 Aug 2020 22:00:53 +0200 Subject: [PATCH] Add graceful API error handling --- src/client.rs | 110 ++++++++++++++++++++++-------- src/client/authenticate.rs | 2 +- src/client/get_home_status.rs | 2 +- src/client/get_homes_data.rs | 2 +- src/client/get_measure.rs | 2 +- src/client/get_station_data.rs | 14 ++-- src/client/set_room_thermpoint.rs | 6 +- src/errors.rs | 23 +++++-- 8 files changed, 118 insertions(+), 43 deletions(-) diff --git a/src/client.rs b/src/client.rs index c4db14e..bf14e6b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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; @@ -65,11 +69,11 @@ impl<'a> UnauthenticatedClient<'a> { .map_err(|e| e.context(ErrorKind::AuthenticationFailed).into()) } - pub(crate) fn call(&self, url: &str, params: &HashMap<&str, &str>) -> Result + pub(crate) fn call(&self, name: &'static str, url: &str, params: &HashMap<&str, &str>) -> Result where T: DeserializeOwned, { - api_call(&self.http, url, params) + api_call(name, &self.http, url, params) } } @@ -83,16 +87,16 @@ impl AuthenticatedClient { &self.token } - pub(crate) fn call<'a, T>(&'a self, url: &str, params: &mut HashMap<&str, &'a str>) -> Result + pub(crate) fn call<'a, T>(&'a self, name: &'static str, url: &str, params: &mut HashMap<&str, &'a str>) -> Result 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(http: &reqwest::Client, url: &str, params: &HashMap<&str, &str>) -> Result +fn api_call(name: &'static str, http: &reqwest::Client, url: &str, params: &HashMap<&str, &str>) -> Result where T: DeserializeOwned, { @@ -100,44 +104,90 @@ where .post(url) .form(¶ms) .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::(&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; +} + +#[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 { + 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 { 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 { - 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 { - 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 { 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::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) } } diff --git a/src/client/authenticate.rs b/src/client/authenticate.rs index cb556ef..ed1c67b 100644 --- a/src/client/authenticate.rs +++ b/src/client/authenticate.rs @@ -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", ¶ms) + unauthenticated_client.call("oauth2/token", "https://api.netatmo.com/oauth2/token", ¶ms) } diff --git a/src/client/get_home_status.rs b/src/client/get_home_status.rs index 4bba3ce..35e42cc 100644 --- a/src/client/get_home_status.rs +++ b/src/client/get_home_status.rs @@ -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 { 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) } diff --git a/src/client/get_homes_data.rs b/src/client/get_homes_data.rs index a67e095..6ee8d8e 100644 --- a/src/client/get_homes_data.rs +++ b/src/client/get_homes_data.rs @@ -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 { 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) } diff --git a/src/client/get_measure.rs b/src/client/get_measure.rs index 620f763..d3a039b 100644 --- a/src/client/get_measure.rs +++ b/src/client/get_measure.rs @@ -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>>, D::Error> diff --git a/src/client/get_station_data.rs b/src/client/get_station_data.rs index c62577b..478a9e9 100644 --- a/src/client/get_station_data.rs +++ b/src/client/get_station_data.rs @@ -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 { @@ -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::*; diff --git a/src/client/set_room_thermpoint.rs b/src/client/set_room_thermpoint.rs index 7b90cfe..5597ee6 100644 --- a/src/client/set_room_thermpoint.rs +++ b/src/client/set_room_thermpoint.rs @@ -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, + ) } diff --git a/src/errors.rs b/src/errors.rs index 716deb2..b4c8aac 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -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 { @@ -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 { @@ -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 }, } } }