use super::auth::{RESTAuthFunction, RESTAuthParameter};
use super::rest_error::RestError;
use crate::Error;
use crate::Result;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::time::Duration;
pub struct HttpClient {
client: reqwest::Client,
base_url: String,
auth_function: Option<RESTAuthFunction>,
}
impl HttpClient {
pub fn new(base_url: &str, auth_function: Option<RESTAuthFunction>) -> Result<Self> {
let final_url = Self::normalize_uri(base_url)?;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| Error::ConfigInvalid {
message: format!("Failed to create HTTP client: {e}"),
})?;
Ok(HttpClient {
client,
base_url: final_url,
auth_function,
})
}
fn normalize_uri(uri: &str) -> Result<String> {
let uri = uri.trim();
if uri.is_empty() {
return Err(Error::ConfigInvalid {
message: "uri is empty which must be defined".to_string(),
});
}
let normalized_url = if uri.starts_with("http://") || uri.starts_with("https://") {
uri.to_string()
} else {
format!("http://{uri}")
};
Ok(normalized_url.trim_end_matches('/').to_string())
}
pub async fn get<T: DeserializeOwned>(
&self,
path: &str,
params: Option<&[(impl AsRef<str>, impl AsRef<str>)]>,
) -> Result<T> {
let url = self.request_url(path);
let params_map: HashMap<String, String> = match params {
Some(p) => p
.iter()
.map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string()))
.collect(),
None => HashMap::new(),
};
let headers = self
.build_auth_headers("GET", path, None, params_map)
.await?;
let mut request = self.client.get(&url);
if let Some(p) = params {
for (key, value) in p {
request = request.query(&[(key.as_ref(), value.as_ref())]);
}
}
let request = Self::apply_headers(request, &headers);
let resp = request.send().await.map_err(|e| Error::UnexpectedError {
message: "http get failed".to_string(),
source: Some(Box::new(e)),
})?;
self.parse_response(resp).await
}
pub async fn post<T: DeserializeOwned, B: serde::Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let url = self.request_url(path);
let body_str = serde_json::to_string(body).ok();
let headers = self
.build_auth_headers("POST", path, body_str.as_deref(), HashMap::new())
.await?;
let request = self.client.post(&url).json(body);
let request = Self::apply_headers(request, &headers);
let resp = request.send().await.map_err(|e| Error::UnexpectedError {
message: "http post failed".to_string(),
source: Some(Box::new(e)),
})?;
self.parse_response(resp).await
}
pub async fn delete<T: DeserializeOwned>(
&self,
path: &str,
params: Option<&[(impl AsRef<str>, impl AsRef<str>)]>,
) -> Result<T> {
let url = self.request_url(path);
let params_map: HashMap<String, String> = match params {
Some(p) => p
.iter()
.map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string()))
.collect(),
None => HashMap::new(),
};
let headers = self
.build_auth_headers("DELETE", path, None, params_map)
.await?;
let mut request = self.client.delete(&url);
if let Some(p) = params {
for (key, value) in p {
request = request.query(&[(key.as_ref(), value.as_ref())]);
}
}
let request = Self::apply_headers(request, &headers);
let resp = request.send().await.map_err(|e| Error::UnexpectedError {
message: "http delete failed".to_string(),
source: Some(Box::new(e)),
})?;
self.parse_response(resp).await
}
pub fn set_auth_function(&mut self, auth_function: RESTAuthFunction) {
self.auth_function = Some(auth_function);
}
async fn build_auth_headers(
&self,
method: &str,
path: &str,
data: Option<&str>,
params: HashMap<String, String>,
) -> Result<HashMap<String, String>> {
if let Some(ref auth_fn) = self.auth_function {
let parameter =
RESTAuthParameter::new(method, path, data.map(|s| s.to_string()), params);
auth_fn.apply(¶meter).await
} else {
Ok(HashMap::new())
}
}
fn apply_headers(
request: reqwest::RequestBuilder,
headers: &HashMap<String, String>,
) -> reqwest::RequestBuilder {
let mut request = request;
for (key, value) in headers {
request = request.header(key, value);
}
request
}
fn request_url(&self, path: &str) -> String {
if path.is_empty() || path == "/" {
self.base_url.clone()
} else if path.starts_with('/') {
format!("{}{}", self.base_url, path)
} else {
format!("{}/{}", self.base_url, path)
}
}
async fn parse_response<T: DeserializeOwned>(&self, resp: reqwest::Response) -> Result<T> {
let status = resp.status();
if !status.is_success() {
let text = resp.text().await.map_err(|e| Error::UnexpectedError {
message: "failed to read response".to_string(),
source: Some(Box::new(e)),
})?;
let error_response: super::ErrorResponse =
RestError::parse_error_response(&text, status.as_u16());
let rest_error: RestError = RestError::from_error_response(error_response);
return Err(Error::from(rest_error));
}
let text = resp.text().await.map_err(|e| Error::UnexpectedError {
message: "failed to read response".to_string(),
source: Some(Box::new(e)),
})?;
if text.trim().is_empty() {
return serde_json::from_str("null").map_err(|e| Error::UnexpectedError {
message: "failed to parse empty response".to_string(),
source: Some(Box::new(e)),
});
}
serde_json::from_str(&text).map_err(|e| Error::UnexpectedError {
message: "failed to parse json".to_string(),
source: Some(Box::new(e)),
})
}
}