use displaydoc::Display;
use serde::{Serialize, de::DeserializeOwned};
use std::{fmt::Debug, pin::Pin};
pub use reqwest::{Error as ReqwestError, Method, Response, StatusCode, header::HeaderMap};
pub use reqwest_middleware::{ClientWithMiddleware, Error as ReqwestMiddlewareError};
pub use serde_json::Error as SerdeJsonError;
pub use serde_qs::Error as SerdeQsError;
pub use url::{ParseError, Url};
pub struct RestClient {
pub client: ClientWithMiddleware,
pub url: Url,
}
impl RestClient {
pub async fn request<S: Serialize, T: DeserializeOwned>(
&self,
method: Method,
route: impl AsRef<str> + Debug,
data: &S,
) -> Result<T, Error> {
self.request_with_headers(method, route, HeaderMap::new(), data)
.await
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, headers, data)))]
pub async fn request_with_headers<S: Serialize, T: DeserializeOwned>(
&self,
method: Method,
route: impl AsRef<str> + Debug,
headers: HeaderMap,
data: &S,
) -> Result<T, Error> {
let mut url = join_url(&self.url, route.as_ref())?;
let data_in_query_string = method == Method::GET || method == Method::DELETE;
if data_in_query_string {
let query = serde_qs::to_string(data)
.map_err(|err| Error::QueryString(method.clone(), url.clone(), Box::new(err)))?;
if !query.is_empty() {
url.set_query(Some(&query));
}
}
let mut req = self.client.request(method.clone(), url.clone());
if !data_in_query_string {
req = req.json(data);
}
req = req.headers(headers);
let resp = req
.send()
.await
.map_err(|err| Error::Request(method.clone(), url.clone(), Box::new(err)))?;
let status = resp.status();
#[cfg(feature = "tracing")]
tracing::debug!("{} {} => {}", method, url, status);
if resp.error_for_status_ref().is_ok() {
let body_text = resp
.text()
.await
.map_err(|err| Error::Request(method.clone(), url.clone(), Box::new(err.into())))?;
let body_str: &str = if body_text.is_empty() {
"null"
} else {
&body_text
};
let body: T =
serde_json::from_str(body_str).map_err(|err| Error::Deserialization(method, url, status, if body_text.is_empty() {
<serde_json::Error as serde::de::Error>::custom("response body text was empty, but 'null' was not a valid value for the expected response schema: {err}")
} else { err }))?;
Ok(body)
} else {
Err(Error::Response(method, url, status, Box::pin(resp)))
}
}
}
#[derive(Debug, Display)]
pub enum Error {
UrlCannotBeABase(Url),
QueryString(Method, Url, Box<SerdeQsError>),
Request(Method, Url, Box<ReqwestMiddlewareError>),
Serialization(Method, Url, SerdeJsonError),
Deserialization(Method, Url, StatusCode, SerdeJsonError),
Response(Method, Url, StatusCode, Pin<Box<Response>>),
}
pub fn join_url(url: &Url, route: &str) -> Result<Url, Error> {
let mut result = url.clone();
{
let Ok(mut path_segments_mut) = result.path_segments_mut() else {
return Err(Error::UrlCannotBeABase(result));
};
path_segments_mut.pop_if_empty();
let route = route.strip_prefix('/').unwrap_or(route);
for segment in route.split('/') {
path_segments_mut.push(segment);
}
}
Ok(result)
}