use crate::error::ApiForgeError;
use reqwest::header::HeaderMap;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fmt::Debug;
use tracing::{debug, error, info, warn};
pub enum DataTransmissionMethod {
QueryParams, Json, FormData, Multipart, }
pub enum AuthenticationMethod {
Bearer, Basic, None, }
#[allow(async_fn_in_trait)]
pub trait ApiRequest<Res>
where
Self: Serialize + Debug,
Res: Default + DeserializeOwned,
{
const ENDPOINT: &'static str;
const METHOD: reqwest::Method = reqwest::Method::GET;
const DATA_TRANSMISSION_METHOD: DataTransmissionMethod = DataTransmissionMethod::QueryParams;
const AUTHENTICATION_METHOD: AuthenticationMethod = AuthenticationMethod::None;
async fn from_response(resp: reqwest::Response) -> Result<Res, ApiForgeError> {
if resp.content_length().unwrap_or(0) == 0
|| resp.status() == reqwest::StatusCode::NO_CONTENT
{
debug!("Response is empty or 204 No Content.");
return Ok(Res::default());
}
if let Some(content_type) = resp.headers().get(reqwest::header::CONTENT_TYPE) {
let content_type_str = content_type.to_str().unwrap_or("");
return if content_type_str.contains("application/json") {
debug!("Parsing response as JSON.");
resp.json().await.map_err(ApiForgeError::ParseError)
} else if content_type_str.contains("text/plain") {
error!("Response content type is text/plain, which is not supported.");
Err(ApiForgeError::UnsupportedContentType(
content_type_str.to_string(),
))
} else if content_type_str.contains("application/xml")
|| content_type_str.contains("text/xml")
{
debug!("Parsing response as XML.");
let text = resp
.text()
.await
.map_err(ApiForgeError::ParseError)?;
let xml = serde_xml_rust::from_str(text.as_str())?;
Ok(xml)
} else {
warn!("Unrecognized content type: {}", content_type_str);
Err(ApiForgeError::UnsupportedContentType(
content_type_str.to_string(),
))
};
}
debug!("Falling back to JSON parsing.");
resp.json::<Res>()
.await
.map_err(ApiForgeError::ParseError)
}
fn multipart_form_data(&self) -> reqwest::multipart::Form {
debug!("Implement multipart_form_data() if needed, or leave empty.");
reqwest::multipart::Form::new()
}
fn generate_request(
&self,
base_url: &str,
headers: Option<HeaderMap>,
token: Option<(String, Option<String>)>,
) -> reqwest::RequestBuilder {
let url = format!("{}{}", base_url, Self::ENDPOINT);
let client = reqwest::Client::new();
let builder = match Self::METHOD {
reqwest::Method::GET => client.get(&url),
reqwest::Method::POST => client.post(&url),
reqwest::Method::PUT => client.put(&url),
reqwest::Method::DELETE => client.delete(&url),
reqwest::Method::PATCH => client.patch(&url),
reqwest::Method::HEAD => client.head(&url),
_ => client.get(&url),
};
let mut request = match Self::DATA_TRANSMISSION_METHOD {
DataTransmissionMethod::QueryParams => builder.query(self),
DataTransmissionMethod::Json => builder.json(self),
DataTransmissionMethod::FormData => builder.form(self),
DataTransmissionMethod::Multipart => builder.multipart(self.multipart_form_data()),
};
if let Some((token, password)) = token {
match Self::AUTHENTICATION_METHOD {
AuthenticationMethod::Basic => request = request.basic_auth(token, password),
AuthenticationMethod::Bearer => request = request.bearer_auth(token),
AuthenticationMethod::None => warn!("No authentication required for this request."),
}
}
if let Some(headers) = headers {
request = request.headers(headers);
}
debug!("Generated request: {:#?}", request);
request
}
async fn send_request(
&self,
base_url: &str,
headers: Option<HeaderMap>,
token: Option<(String, Option<String>)>,
) -> reqwest::Result<reqwest::Response> {
info!("Sending request to {}{}...", base_url, Self::ENDPOINT);
debug!("Request body: {:#?}", self);
self.generate_request(base_url, headers, token).send().await
}
async fn send_and_parse(
&self,
base_url: &str,
headers: Option<HeaderMap>,
token: Option<(String, Option<String>)>,
) -> Result<Res, ApiForgeError> {
let response = self.send_request(base_url, headers, token).await?;
if response.error_for_status_ref().is_err() {
Err(ApiForgeError::ResponseError(response.status()))
} else {
Ok(Self::from_response(response).await?)
}
}
}