use std::collections::HashMap;
use std::time::Duration;
use crate::{
from_json, get_driver, FetchMethod, JsJson, JsJsonDeserialize, JsJsonSerialize, LazyCache,
};
#[derive(Debug, Clone)]
pub enum RequestBody {
Text(String),
Json(JsJson),
Binary(Vec<u8>),
}
impl RequestBody {
pub fn into<T: JsJsonDeserialize>(self) -> Result<T, String> {
match self {
RequestBody::Json(json) => match from_json::<T>(json) {
Ok(data) => Ok(data),
Err(err) => Err(err),
},
RequestBody::Text(_) => {
Err("FetchBody.into() - expected json, received text".to_string())
}
RequestBody::Binary(_) => {
Err("FetchBody.into() - expected json, received binary".to_string())
}
}
}
}
#[derive(Clone)]
pub struct RequestBuilder {
method: FetchMethod,
url: String,
headers: HashMap<String, String>,
body: Option<RequestBody>,
ttl: Option<Duration>,
}
impl RequestBuilder {
pub fn new(method: FetchMethod, url: impl Into<String>) -> Self {
Self {
method,
url: url.into(),
headers: HashMap::new(),
body: None,
ttl: None,
}
}
#[must_use]
pub fn get(url: impl Into<String>) -> Self {
Self::new(FetchMethod::GET, url)
}
#[must_use]
pub fn post(url: impl Into<String>) -> Self {
Self::new(FetchMethod::POST, url)
}
#[must_use]
pub fn body(mut self, body: RequestBody) -> Self {
self.body = Some(body);
self
}
#[must_use]
pub fn bearer_auth(self, token: impl Into<String>) -> Self {
let token: String = token.into();
self.set_header("Authorization", format!("Bearer {token}"))
}
#[must_use]
pub fn set_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
let name: String = name.into();
let value: String = value.into();
self.headers.insert(name, value);
self
}
#[must_use]
pub fn body_json(self, body: impl JsJsonSerialize) -> Self {
let body = body.to_json();
self.body(RequestBody::Json(body))
}
#[must_use]
pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers = headers;
self
}
#[must_use]
pub fn ttl_seconds(mut self, seconds: u64) -> Self {
self.ttl = Some(Duration::from_secs(seconds));
self
}
#[must_use]
pub fn ttl_minutes(mut self, minutes: u64) -> Self {
self.ttl = Some(Duration::from_secs(minutes * 60));
self
}
#[must_use]
pub fn ttl_hours(mut self, hours: u64) -> Self {
self.ttl = Some(Duration::from_secs(hours * 60 * 60));
self
}
#[must_use]
pub fn ttl_days(mut self, days: u64) -> Self {
self.ttl = Some(Duration::from_secs(days * 24 * 60 * 60));
self
}
#[must_use]
pub fn get_ttl(&self) -> Option<Duration> {
self.ttl
}
pub async fn call(&self) -> RequestResponse {
let Self {
method,
url,
headers,
body,
ttl: _,
} = self;
let result = get_driver()
.inner
.api
.fetch(*method, url.clone(), Some(headers.clone()), body.clone())
.await;
RequestResponse::new(*method, url.clone(), result)
}
#[must_use]
pub fn lazy_cache<T>(
self,
map_response: impl Fn(u32, RequestBody) -> Option<Result<T, String>> + 'static,
) -> LazyCache<T> {
LazyCache::new(self, map_response)
}
}
#[derive(Debug)]
pub struct RequestResponse {
method: FetchMethod,
url: String,
data: Result<(u32, RequestBody), String>,
}
impl RequestResponse {
fn new(
method: FetchMethod,
url: String,
data: Result<(u32, RequestBody), String>,
) -> RequestResponse {
RequestResponse { method, url, data }
}
pub fn status(&self) -> Option<u32> {
if let Ok((status, _)) = self.data {
return Some(status);
}
None
}
pub fn into<T>(
self,
convert: impl Fn(u32, RequestBody) -> Option<Result<T, String>>,
) -> Result<T, String> {
let result = match self.data {
Ok((status, body)) => match convert(status, body) {
Some(result) => result,
None => Err(format!("Unhandled response code {status}")),
},
Err(err) => Err(err),
};
if let Err(err) = &result {
log::error!(
"Error fetching {} {}: {}",
self.method.to_str(),
self.url,
err
);
}
result
}
pub fn into_data<T: JsJsonDeserialize>(self) -> Result<T, String> {
self.into(|_, response_body| Some(response_body.into::<T>()))
}
pub fn into_error_message<T>(self) -> Result<T, String> {
let body = match self.data {
Ok((code, body)) => format!("API error {code}: {body:#?}"),
Err(body) => format!("Network error: {body}"),
};
Err(body)
}
}