use std::fmt::Debug;
use std::str::FromStr;
use std::time::Duration;
use reqwest::{Identity, Response, Url};
use serde::Serialize;
use thiserror::Error;
use tokio::{fs::File, io::AsyncReadExt};
use tracing::instrument;
use url::Host;
use crate::error::{Error, ErrorResponse};
#[derive(Debug)]
pub struct RestClientBuilder<'i> {
identity_cert_file: &'i str,
environment: Environment,
connect_timeout: Duration,
timeout: Duration,
min_tls_version: reqwest::tls::Version,
}
impl<'i> RestClientBuilder<'i> {
#[instrument]
pub fn new(identity_cert_file: &'i str, environment: Environment) -> Self {
Self {
identity_cert_file,
environment,
connect_timeout: Duration::from_secs(10),
timeout: Duration::from_secs(30),
min_tls_version: reqwest::tls::Version::TLS_1_2,
}
}
pub fn connect_timeout(&mut self, duration: Duration) -> &mut Self {
self.connect_timeout = duration;
self
}
pub fn timeout(&mut self, duration: Duration) -> &mut Self {
self.timeout = duration;
self
}
pub fn min_tls_version(&mut self, version: reqwest::tls::Version) -> &mut Self {
self.min_tls_version = version;
self
}
#[instrument]
pub async fn build(&self) -> crate::Result<RestClient> {
let mut cert = Vec::new();
File::open(self.identity_cert_file)
.await
.map_err(|source| Error::OpenIdentityCertFile {
path: self.identity_cert_file.into(),
source,
})?
.read_to_end(&mut cert)
.await
.map_err(|source| Error::ReadIdentityCertFile {
path: self.identity_cert_file.into(),
source,
})?;
let identity =
Identity::from_pem(&cert).map_err(|source| Error::ParseIdentityCertFile {
path: self.identity_cert_file.into(),
source,
})?;
let client = reqwest::ClientBuilder::new()
.identity(identity)
.connect_timeout(self.connect_timeout)
.timeout(self.timeout)
.min_tls_version(self.min_tls_version)
.build()
.map_err(Error::BuildRequestClient)?;
Ok(RestClient {
client,
host: self.environment.domain(),
})
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Environment {
Test,
Acceptance,
Staging,
Production,
}
#[derive(Error, Debug)]
pub enum ParseEnvironmentError {
#[error("'{0}' is not a valid environment string")]
InvalidEnvironmentString(String),
}
impl FromStr for Environment {
type Err = ParseEnvironmentError;
#[instrument]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"test" => Self::Test,
"acceptance" => Self::Acceptance,
"staging" => Self::Staging,
"production" => Self::Production,
s => return Err(ParseEnvironmentError::InvalidEnvironmentString(s.into())),
})
}
}
impl Environment {
pub fn domain(&self) -> Host<&'static str> {
match self {
Environment::Test => Host::Domain("test-rest.basispoort.nl"),
Environment::Acceptance => Host::Domain("acceptatie-rest.basispoort.nl"),
Environment::Staging => Host::Domain("staging-rest.basispoort.nl"),
Environment::Production => Host::Domain("rest.basispoort.nl"),
}
}
}
#[derive(Debug)]
pub struct RestClient {
client: reqwest::Client,
pub host: Host<&'static str>,
}
impl RestClient {
#[instrument]
fn make_url(&self, path: &str) -> crate::Result<Url> {
let url = format!("https://{}{}", &self.host, &path);
Url::parse(&url).map_err(|source| Error::ParseUrl { url, source }.into())
}
#[instrument]
async fn error_status(&self, url: Url, response: Response) -> crate::Result<Response> {
let status = response.status();
match response.error_for_status_ref() {
Err(source) => {
let response_bytes = response.bytes().await.map_err(Error::ReceiveResponseBody)?;
let error_response = match serde_json::from_slice(&response_bytes) {
Ok(error_response) => ErrorResponse::JSON(error_response),
Err(_) => ErrorResponse::Plain(String::from_utf8_lossy(&response_bytes).into()),
};
Err(Error::HttpResponse {
url,
status,
error_response,
source,
}
.into())
}
Ok(_) => Ok(response),
}
}
#[instrument]
pub async fn get(&self, path: &str) -> crate::Result<Response> {
let url = self.make_url(path)?;
let response = self
.client
.get(url.clone())
.send()
.await
.map_err(Error::HttpRequest)?;
self.error_status(url, response).await
}
#[instrument]
pub async fn post<T: Serialize + Debug + ?Sized>(
&self,
path: &str,
payload: &T,
) -> crate::Result<Response> {
let url = self.make_url(path)?;
let response = self
.client
.post(url.clone())
.json(payload)
.send()
.await
.map_err(Error::HttpRequest)?;
self.error_status(url, response).await
}
#[instrument]
pub async fn put<T: Serialize + Debug + ?Sized>(
&self,
path: &str,
payload: &T,
) -> crate::Result<Response> {
let url = self.make_url(path)?;
let response = self
.client
.put(url.clone())
.json(payload)
.send()
.await
.map_err(Error::HttpRequest)?;
self.error_status(url, response).await
}
#[instrument]
pub async fn delete(&self, path: &str) -> crate::Result<Response> {
let url = self.make_url(path)?;
let response = self
.client
.delete(url.clone())
.send()
.await
.map_err(Error::HttpRequest)?;
self.error_status(url, response).await
}
}
#[cfg(test)]
mod tests {
}