use crate::fetch::FetchError;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::marker::PhantomData;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response, Window};
pub enum MethodBody<'a, T> {
Head,
Get,
Delete,
Post(&'a T),
Put(&'a T),
Patch(&'a T),
}
impl<'a, T> MethodBody<'a, T> {
pub fn as_method(&self) -> &'static str {
match self {
MethodBody::Get => "GET",
MethodBody::Delete => "DELETE",
MethodBody::Post(_) => "POST",
MethodBody::Put(_) => "PUT",
MethodBody::Patch(_) => "PATCH",
MethodBody::Head => "HEAD",
}
}
}
impl<'a, T: Serialize> MethodBody<'a, T> {
pub fn as_body<FORMAT: Format>(&self) -> Result<Option<JsValue>, FetchError> {
let body: Option<String> = match self {
MethodBody::Get | MethodBody::Delete | MethodBody::Head => None,
MethodBody::Put(data) | MethodBody::Post(data) | MethodBody::Patch(data) => {
let body =
FORMAT::serialize(data).ok_or(FetchError::CouldNotSerializeRequestBody)?;
Some(body)
}
};
let body = body.map(|data| JsValue::from_str(data.as_str()));
Ok(body)
}
}
pub trait Format {
fn serialize<T: Serialize>(t: &T) -> Option<String>;
fn deserialize<T: DeserializeOwned>(s: &str) -> Option<T>;
}
pub struct Json;
impl Format for Json {
fn serialize<T: Serialize>(t: &T) -> Option<String> {
serde_json::to_string(t).ok()
}
fn deserialize<T: DeserializeOwned>(s: &str) -> Option<T> {
serde_json::from_str(s).ok()
}
}
pub trait FetchRequest {
type RequestBody: Serialize;
type ResponseBody: DeserializeOwned;
type Format: Format;
fn url(&self) -> String;
fn method(&self) -> MethodBody<Self::RequestBody>;
fn headers(&self) -> Vec<(String, String)>;
fn use_cors(&self) -> bool {
false
}
}
pub fn create_request<T: FetchRequest>(request: &T) -> Result<Request, FetchError> {
let method = request.method();
let headers = request.headers();
let headers = JsValue::from_serde(&headers).expect("Convert Headers to Tuple");
let mut opts = RequestInit::new();
opts.method(method.as_method());
opts.body(method.as_body::<T::Format>()?.as_ref());
opts.headers(&headers);
if request.use_cors() {
opts.mode(RequestMode::Cors);
}
Request::new_with_str_and_init(&request.url(), &opts).map_err(FetchError::CouldNotCreateRequest)
}
pub async fn fetch_resource<T: FetchRequest>(
request: Result<Request, FetchError>,
_req_type: PhantomData<T>,
) -> Result<T::ResponseBody, FetchError> {
let request = request?;
let window: Window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|_| FetchError::CouldNotCreateFetchFuture)?;
debug_assert!(resp_value.is_instance_of::<Response>());
let resp: Response = resp_value.dyn_into().unwrap();
let text = JsFuture::from(resp.text().map_err(|_| FetchError::TextNotAvailable)?)
.await
.map_err(|_| FetchError::TextNotAvailable)?;
let text_string = text.as_string().unwrap();
if !resp.ok() {
return Err(FetchError::ResponseError {
status_code: resp.status(),
response_body: text_string,
});
}
let deserialized =
<T::Format>::deserialize(&text_string).ok_or_else(|| FetchError::DeserializeError {
error: "".to_string(),
content: text_string,
})?;
Ok(deserialized)
}