use std::collections::BTreeMap;
use std::time::Duration;
use crate::{
Computed, Context, FetchMethod, JsJson, JsJsonDeserialize, JsJsonSerialize, LazyCache, Value,
dev::{SsrFetchRequest, SsrFetchRequestBody, SsrFetchResponse, SsrFetchResponseContent},
driver_module::api::api_fetch,
from_json, transaction,
};
#[derive(Debug, Clone)]
pub enum RequestBody {
Json(JsJson),
}
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),
},
}
}
}
#[derive(Clone)]
pub struct RequestBuilder {
method: FetchMethod,
url: String,
headers: BTreeMap<String, String>,
bearer_auth: Computed<Option<String>>,
body: Option<RequestBody>,
ttl: Option<Duration>,
}
impl RequestBuilder {
pub fn new(method: FetchMethod, url: impl Into<String>) -> Self {
let init_bearer = Value::<Option<String>>::new(None);
Self {
method,
url: url.into(),
headers: BTreeMap::new(),
bearer_auth: init_bearer.to_computed(),
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
}
pub fn get_bearer_auth(&self) -> Computed<Option<String>> {
self.bearer_auth.clone()
}
#[must_use]
pub fn bearer_auth(mut self, token: impl Into<Computed<Option<String>>>) -> Self {
self.bearer_auth = token.into();
self
}
#[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: BTreeMap<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 fn to_request(self, token: Option<String>) -> SsrFetchRequest {
let mut headers = self.headers;
let body = match self.body {
None => SsrFetchRequestBody::None,
Some(RequestBody::Json(data)) => {
if !headers.contains_key("Content-Type") {
headers.insert(
"Content-Type".into(),
"application/json;charset=UTF-8".into(),
);
}
SsrFetchRequestBody::Data { data }
}
};
if let Some(token) = token {
headers.insert("Authorization".into(), format!("Bearer {token}"));
}
SsrFetchRequest {
method: self.method,
url: self.url,
headers,
body,
}
}
pub fn to_request_context(self, context: &Context) -> SsrFetchRequest {
let token = self.bearer_auth.get(context);
self.to_request(token)
}
pub async fn call(self) -> RequestResponse {
let token = transaction(|context| self.get_bearer_auth().get(context));
let request = self.to_request(token);
let result = api_fetch().fetch(request.clone()).await;
RequestResponse::new(request, result)
}
#[must_use]
pub fn lazy_cache<T: PartialEq>(
self,
map_response: impl Fn(u32, RequestBody) -> Option<Result<T, String>> + 'static,
) -> LazyCache<T> {
LazyCache::new(self, map_response)
}
}
#[derive(Debug)]
pub struct RequestResponse {
request: SsrFetchRequest,
response: SsrFetchResponse,
}
impl RequestResponse {
pub fn new(request: SsrFetchRequest, response: SsrFetchResponse) -> RequestResponse {
RequestResponse { request, response }
}
pub fn status(&self) -> Option<u32> {
if let SsrFetchResponse::Ok {
status,
response: _,
} = &self.response
{
return Some(*status);
}
None
}
pub fn into<T>(
self,
convert: impl Fn(u32, RequestBody) -> Option<Result<T, String>>,
) -> Result<T, String> {
let result: Result<T, String> = match self.response {
SsrFetchResponse::Ok { status, response } => {
let data = match response {
SsrFetchResponseContent::Json(json_response) => {
convert(status, RequestBody::Json(json_response))
}
SsrFetchResponseContent::Text(_) => {
return Err("Tried to decode text/plain reponse".to_string());
}
};
match data {
Some(result) => result,
None => Err(format!("Unhandled response code {status}")),
}
}
SsrFetchResponse::Err { message } => Err(message),
};
if let Err(err) = &result {
log::error!(
"Error fetching {} {}: {}",
self.request.method.to_str(),
self.request.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.response {
SsrFetchResponse::Ok { status, response } => {
format!("API error {status}: {response:#?}")
}
SsrFetchResponse::Err { message } => format!("Network error: {message}"),
};
Err(body)
}
}