use crate::client::{Result, API_BASE};
use reduct_base::error::{ErrorCode, IntEnum, ReductError};
use reqwest::header::HeaderValue;
use reqwest::{Method, RequestBuilder, Response, Url};
pub(crate) struct HttpClient {
base_url: Url,
api_token: String,
client: reqwest::Client,
}
impl HttpClient {
pub(super) fn new(base_url: &str, api_token: &str, client: reqwest::Client) -> Result<Self> {
Ok(Self {
base_url: Url::parse(base_url)?,
api_token: format!("Bearer {}", api_token),
client,
})
}
pub fn url(&self) -> &str {
&self.base_url.as_str()[..self.base_url.as_str().len() - API_BASE.len()]
}
pub fn api_token(&self) -> &str {
&self.api_token[7..] }
pub async fn send_and_receive_json<In, Out>(
&self,
method: Method,
path: &str,
data: Option<In>,
) -> Result<Out>
where
In: serde::Serialize,
Out: for<'de> serde::Deserialize<'de>,
{
let mut request = self.request(method, path);
if let Some(body) = data {
request = request.json(&body);
}
let response = self.send_request(request).await?;
response.json::<Out>().await.map_err(|e| {
ReductError::new(
ErrorCode::Unknown,
&format!("Failed to parse response: {}", e),
)
})
}
pub async fn send_json<In>(&self, method: Method, path: &str, data: In) -> Result<()>
where
In: serde::Serialize,
{
let request = self.request(method, path).json(&data);
self.send_request(request).await?;
Ok(())
}
pub fn request(&self, method: Method, path: &str) -> RequestBuilder {
self.client
.request(method, format!("{}{}", self.base_url, path))
.header("Authorization", &self.api_token)
}
pub async fn send_request(&self, request: RequestBuilder) -> Result<Response> {
let response = match request.send().await {
Ok(response) => {
if response.status().is_success() {
Self::check_server_api_version(&response)?;
Ok(response)
} else {
let status = ErrorCode::from_int(response.status().as_u16() as i16)
.unwrap_or(ErrorCode::Unknown);
let error_msg = response
.headers()
.get("x-reduct-error")
.unwrap_or(&HeaderValue::from_str("Unknown").unwrap())
.to_str()
.unwrap()
.to_string();
Err(ReductError::new(status, &error_msg))
}
}
Err(e) => Err(map_error(e)),
};
response
}
fn check_server_api_version(response: &Response) -> Result<()> {
let Some(api_version) = response
.headers()
.get("x-reduct-api")
.and_then(|v| v.to_str().ok())
else {
return Err(ReductError::new(
ErrorCode::Unknown,
"Missing x-reduct-api header",
));
};
let (major, minor) = api_version
.split_once('.')
.ok_or_else(|| ReductError::new(ErrorCode::Unknown, "Invalid x-reduct-api header"))?;
let major = major
.parse::<u32>()
.map_err(|_| ReductError::new(ErrorCode::Unknown, "Invalid x-reduct-api header"))?;
let minor = minor
.parse::<u32>()
.map_err(|_| ReductError::new(ErrorCode::Unknown, "Invalid x-reduct-api header"))?;
if major != env!("CARGO_PKG_VERSION_MAJOR").parse::<u32>().unwrap() {
return Err(ReductError::new(
ErrorCode::Unknown,
&format!(
"Unsupported API version: {}.{} (client: {}.{})",
major,
minor,
env!("CARGO_PKG_VERSION_MAJOR"),
env!("CARGO_PKG_VERSION_MINOR")
),
));
}
if minor + 2 < env!("CARGO_PKG_VERSION_MINOR").parse().unwrap() {
eprintln!(
"Warning: Server API version is too old: {}, please update the server to {}.{}",
api_version,
env!("CARGO_PKG_VERSION_MAJOR"),
env!("CARGO_PKG_VERSION_MINOR")
);
}
Ok(())
}
}
pub(crate) fn map_error(error: reqwest::Error) -> ReductError {
let status = if error.is_connect() {
ErrorCode::ConnectionError
} else if error.is_timeout() {
ErrorCode::Timeout
} else {
ErrorCode::Unknown
};
ReductError::new(status, &error.to_string())
}