use anyhow::Context;
use reqwest::{Client, StatusCode};
use response::ApiResponse;
use serde::{de::DeserializeOwned, Serialize};
use crate::{
client::errors::{ClientError, RequestFailedError},
schema::WebError,
};
use super::{
authentication::Authentication,
errors::{
AuthenticationError, ParameterError, ResourceNotFoundError, ServerError, UnknownError,
},
Result,
};
pub(crate) mod response;
pub(crate) struct InternalWebClient {
pub(crate) client: Client,
pub(crate) auth: Authentication,
}
#[expect(unused)]
pub(crate) enum HttpRequestType {
Post,
Get,
Put,
Patch,
Delete,
}
impl InternalWebClient {
pub fn new(auth: Authentication) -> Self {
Self {
client: Client::new(),
auth,
}
}
async fn send_request_internal<I: Serialize>(
&self,
url: &str,
request_type: HttpRequestType,
request_body: I,
) -> Result<(String, StatusCode)> {
let mut request_builder = match request_type {
HttpRequestType::Post => self.client.post(url),
HttpRequestType::Get => self.client.get(url),
HttpRequestType::Put => self.client.put(url),
HttpRequestType::Patch => self.client.patch(url),
HttpRequestType::Delete => self.client.delete(url),
}
.json(&request_body);
match &self.auth {
Authentication::None => {}
Authentication::Application(application_auth) => {
request_builder = request_builder.bearer_auth(application_auth.jwt.clone());
}
Authentication::Organisation(_organisation_auth) => {
todo!("Organisation API Keys not yet implemented.")
}
}
let response = request_builder.send().await?;
let status = response.status();
let response_body = response.text().await?;
Ok((response_body, status))
}
pub(crate) async fn send_request_no_data<I: Serialize>(
&self,
url: &str,
request_type: HttpRequestType,
input_body: I,
) -> Result<()> {
let (response_body, response_status_code) = self
.send_request_internal(url, request_type, input_body)
.await?;
let response: ApiResponse<()> = serde_json::from_str(response_body.as_str())
.with_context(|| {
format!(
"Unable to deserialise response. response body was: \"{}\"",
response_body
)
})
.map_err(|error| -> ClientError { RequestFailedError::new(error.into()).into() })?;
match response {
ApiResponse::Success(_) => Ok(()),
ApiResponse::Error(error_response) => Err(map_error_response(
response_status_code,
error_response.errors,
)),
}
}
pub(crate) async fn send_request_with_data<I: Serialize, O: DeserializeOwned + 'static>(
&self,
url: &str,
request_type: HttpRequestType,
input_body: I,
) -> Result<O> {
let (response_body, response_status_code) = self
.send_request_internal(url, request_type, input_body)
.await?;
let response: ApiResponse<O> = serde_json::from_str(response_body.as_str())
.with_context(|| {
format!(
"Unable to deserialise response. response body was: \"{}\"",
response_body
)
})
.map_err(|error| -> ClientError { RequestFailedError::new(error.into()).into() })?;
match response {
ApiResponse::Success(success_response) => {
let data = success_response.data.ok_or(RequestFailedError::new(
"response did not contain data".into(),
))?;
Ok(data)
}
ApiResponse::Error(error_response) => Err(map_error_response(
response_status_code,
error_response.errors,
)),
}
}
}
fn map_error_response(status_code: StatusCode, errors: Vec<WebError>) -> ClientError {
match status_code {
StatusCode::BAD_REQUEST
| StatusCode::PAYMENT_REQUIRED
| StatusCode::METHOD_NOT_ALLOWED
| StatusCode::NOT_ACCEPTABLE
| StatusCode::CONFLICT
| StatusCode::LENGTH_REQUIRED
| StatusCode::PRECONDITION_FAILED
| StatusCode::PAYLOAD_TOO_LARGE
| StatusCode::URI_TOO_LONG
| StatusCode::UNSUPPORTED_MEDIA_TYPE
| StatusCode::RANGE_NOT_SATISFIABLE
| StatusCode::EXPECTATION_FAILED
| StatusCode::UNPROCESSABLE_ENTITY
| StatusCode::FAILED_DEPENDENCY
| StatusCode::UPGRADE_REQUIRED
| StatusCode::PRECONDITION_REQUIRED
| StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE => ParameterError::from(errors).into(),
StatusCode::UNAUTHORIZED
| StatusCode::FORBIDDEN
| StatusCode::PROXY_AUTHENTICATION_REQUIRED => AuthenticationError::from(errors).into(),
StatusCode::REQUEST_TIMEOUT
| StatusCode::MISDIRECTED_REQUEST
| StatusCode::TOO_MANY_REQUESTS => RequestFailedError::from(errors).into(),
StatusCode::NOT_FOUND
| StatusCode::GONE
| StatusCode::LOCKED
| StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => ResourceNotFoundError::from(errors).into(),
_ => {
if status_code.is_server_error() {
ServerError::from(errors).into()
} else {
UnknownError::from(errors).into()
}
}
}
}