use std::future::Future;
use gloo_timers::callback::Timeout;
use serde::{de::DeserializeOwned, Serialize};
use serde_json;
use std::{borrow::Cow, cell::RefCell, collections::HashMap, convert::identity, rc::Rc};
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys;
pub type DomException = web_sys::DomException;
pub type ResponseResult<T> = Result<Response<T>, FailReason<T>>;
pub type ResponseDataResult<T> = Result<T, FailReason<T>>;
#[allow(clippy::module_name_repetitions)]
pub type FetchResult<T> = Result<ResponseWithDataResult<T>, RequestError>;
pub type DataResult<T> = Result<T, DataError>;
type Json = String;
#[derive(Debug, Clone)]
#[allow(clippy::module_name_repetitions)]
pub struct FetchObject<T> {
pub request: Request,
pub result: FetchResult<T>,
}
impl<T> FetchObject<T> {
#[allow(clippy::missing_errors_doc)]
pub fn response(self) -> ResponseResult<T> {
let response = match self.result {
Err(ref request_error) => {
return Err(FailReason::RequestError(request_error.clone(), self))
}
Ok(ref response) => response,
};
if response.status.is_error() {
return Err(FailReason::Status(response.status.clone(), self));
}
if let Err(ref data_error) = response.data {
return Err(FailReason::DataError(data_error.clone(), self));
}
let response = self.result.unwrap();
Ok(Response {
raw: response.raw,
status: response.status,
data: response.data.unwrap(),
})
}
#[allow(clippy::missing_errors_doc)]
pub fn response_data(self) -> ResponseDataResult<T> {
self.response().map(|response| response.data)
}
}
#[derive(Debug, Clone)]
pub enum FailReason<T> {
RequestError(RequestError, FetchObject<T>),
Status(Status, FetchObject<T>),
DataError(DataError, FetchObject<T>),
}
#[derive(Debug, Clone)]
pub enum RequestError {
DomException(web_sys::DomException),
}
#[derive(Debug, Clone)]
pub enum DataError {
DomException(web_sys::DomException),
SerdeError(Rc<serde_json::Error>, Json),
}
#[derive(Debug, Clone)]
pub struct RequestController {
abort_controller: Rc<web_sys::AbortController>,
timeout_handle: Rc<RefCell<Option<Timeout>>>,
}
impl RequestController {
pub fn abort(&self) {
self.timeout_handle.replace(None);
self.abort_controller.abort();
}
pub fn disable_timeout(&self) -> Result<(), &'static str> {
match self.timeout_handle.replace(None) {
Some(_) => Ok(()),
None => Err("disable_timeout: already disabled"),
}
}
}
impl Default for RequestController {
fn default() -> Self {
Self {
abort_controller: Rc::new(
web_sys::AbortController::new().expect("fetch: create AbortController - failed"),
),
timeout_handle: Rc::new(RefCell::new(None)),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum StatusCategory {
Informational,
Success,
Redirection,
ClientError,
ServerError,
Unknown,
}
#[derive(Debug, Clone)]
pub struct Status {
pub code: u16,
pub text: String,
pub category: StatusCategory,
}
#[allow(dead_code)]
impl Status {
pub fn is_error(&self) -> bool {
match self.category {
StatusCategory::ClientError | StatusCategory::ServerError => true,
_ => false,
}
}
pub fn is_ok(&self) -> bool {
self.category == StatusCategory::Success
}
}
impl From<&web_sys::Response> for Status {
fn from(response: &web_sys::Response) -> Self {
let text = response.status_text();
match response.status() {
code @ 100..=199 => Status {
code,
text,
category: StatusCategory::Informational,
},
code @ 200..=299 => Status {
code,
text,
category: StatusCategory::Success,
},
code @ 300..=399 => Status {
code,
text,
category: StatusCategory::Redirection,
},
code @ 400..=499 => Status {
code,
text,
category: StatusCategory::ClientError,
},
code @ 500..=599 => Status {
code,
text,
category: StatusCategory::ServerError,
},
code => Status {
code,
text,
category: StatusCategory::Unknown,
},
}
}
}
#[derive(Debug, Clone)]
pub struct Response<T> {
pub raw: web_sys::Response,
pub status: Status,
pub data: T,
}
#[derive(Debug, Clone)]
pub struct ResponseWithDataResult<T> {
pub raw: web_sys::Response,
pub status: Status,
pub data: DataResult<T>,
}
#[derive(Debug, Clone, Copy)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
}
impl Method {
fn as_str(&self) -> &str {
match *self {
Method::Get => "GET",
Method::Head => "HEAD",
Method::Post => "POST",
Method::Put => "PUT",
Method::Delete => "DELETE",
Method::Connect => "CONNECT",
Method::Options => "OPTIONS",
Method::Trace => "TRACE",
Method::Patch => "PATCH",
}
}
}
impl Default for Method {
fn default() -> Self {
Method::Get
}
}
#[derive(Debug, Clone, Default)]
pub struct Request {
url: Cow<'static, str>,
headers: HashMap<String, String>,
method: Method,
body: Option<JsValue>,
cache: Option<web_sys::RequestCache>,
credentials: Option<web_sys::RequestCredentials>,
integrity: Option<String>,
mode: Option<web_sys::RequestMode>,
redirect: Option<web_sys::RequestRedirect>,
referrer: Option<String>,
referrer_policy: Option<web_sys::ReferrerPolicy>,
timeout: Option<u32>,
controller: RequestController,
}
impl Request {
pub fn new(url: impl Into<Cow<'static, str>>) -> Self {
Self {
url: url.into(),
..Self::default()
}
}
pub const fn method(mut self, method: Method) -> Self {
self.method = method;
self
}
pub fn header(mut self, name: &str, value: &str) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn body(mut self, body: JsValue) -> Self {
self.body = Some(body);
self
}
pub fn body_json<T: Serialize>(self, body_json: &T) -> Self {
let json =
serde_json::to_string(body_json).expect("fetch: serialize body to JSON - failed");
let json_as_js_value = JsValue::from_str(&json);
self.body(json_as_js_value)
}
pub fn send_json<T: Serialize>(self, data: &T) -> Self {
self.header("Content-Type", "application/json; charset=utf-8")
.body_json(data)
}
pub fn cache(mut self, cache: web_sys::RequestCache) -> Self {
self.cache = Some(cache);
self
}
pub fn credentials(mut self, request_credentials: web_sys::RequestCredentials) -> Self {
self.credentials = Some(request_credentials);
self
}
pub fn integrity(mut self, integrity: &str) -> Self {
self.integrity = Some(integrity.into());
self
}
pub fn mode(mut self, mode: web_sys::RequestMode) -> Self {
self.mode = Some(mode);
self
}
pub fn redirect(mut self, redirect: web_sys::RequestRedirect) -> Self {
self.redirect = Some(redirect);
self
}
pub fn referrer(mut self, referrer: String) -> Self {
self.referrer = Some(referrer);
self
}
pub fn referrer_policy(mut self, referrer_policy: web_sys::ReferrerPolicy) -> Self {
self.referrer_policy = Some(referrer_policy);
self
}
pub fn timeout(mut self, millis: u32) -> Self {
self.timeout = Some(millis);
self
}
pub fn controller(self, controller_transferrer: impl FnOnce(RequestController)) -> Self {
controller_transferrer(self.controller.clone());
self
}
pub async fn fetch<U>(self, f: impl FnOnce(FetchObject<()>) -> U) -> Result<U, U>
where
U: 'static,
{
let fetch_result = self
.send_request()
.await
.map(|raw_response: web_sys::Response| ResponseWithDataResult {
status: Status::from(&raw_response),
raw: raw_response,
data: Ok(()),
})
.map_err(|js_value_error| RequestError::DomException(js_value_error.into()));
Ok(f(FetchObject {
request: self,
result: fetch_result,
}))
}
pub async fn fetch_string<U>(self, f: impl FnOnce(FetchObject<String>) -> U) -> Result<U, U>
where
U: 'static,
{
let fetch_object = self.fetch(identity).await.unwrap();
let fetch_result = fetch_object.result;
let request = fetch_object.request;
let fetch_object = match fetch_result {
Err(request_error) => FetchObject::<String> {
request,
result: Err(request_error),
},
Ok(response) => {
match response.raw.text() {
Err(js_value_error) => FetchObject::<String> {
request,
result: Ok(ResponseWithDataResult {
raw: response.raw,
status: response.status,
data: Err(DataError::DomException(js_value_error.into())),
}),
},
Ok(promise) => {
let js_future_result = JsFuture::from(promise).await;
match js_future_result {
Err(js_value_error) => FetchObject::<String> {
request,
result: Ok(ResponseWithDataResult {
raw: response.raw,
status: response.status,
data: Err(DataError::DomException(js_value_error.into())),
}),
},
Ok(js_value) => {
let text = js_value
.as_string()
.expect("fetch: cannot convert js_value to string");
FetchObject::<String> {
request,
result: Ok(ResponseWithDataResult {
raw: response.raw,
status: response.status,
data: Ok(text),
}),
}
}
}
}
}
}
};
Ok(f(fetch_object))
}
pub fn fetch_string_data<U>(
self,
f: impl FnOnce(ResponseDataResult<String>) -> U,
) -> impl Future<Output = Result<U, U>>
where
U: 'static,
{
self.fetch_string(|fetch_object| f(fetch_object.response_data()))
}
pub async fn fetch_json<T, U>(self, f: impl FnOnce(FetchObject<T>) -> U) -> Result<U, U>
where
T: DeserializeOwned + 'static,
U: 'static,
{
let fetch_object = self.fetch_string(identity).await.unwrap();
let fetch_result = fetch_object.result;
let request = fetch_object.request;
let fetch_object = match fetch_result {
Err(request_error) => FetchObject::<T> {
request,
result: Err(request_error),
},
Ok(response) => {
match response.data {
Err(data_error) => FetchObject::<T> {
request,
result: Ok(ResponseWithDataResult {
raw: response.raw,
status: response.status,
data: Err(data_error),
}),
},
Ok(text) => {
match serde_json::from_str(&text) {
Err(serde_error) => FetchObject::<T> {
request,
result: Ok(ResponseWithDataResult {
raw: response.raw,
status: response.status,
data: Err(DataError::SerdeError(Rc::new(serde_error), text)),
}),
},
Ok(value) => FetchObject::<T> {
request,
result: Ok(ResponseWithDataResult {
raw: response.raw,
status: response.status,
data: Ok(value),
}),
},
}
}
}
}
};
Ok(f(fetch_object))
}
pub fn fetch_json_data<T, U>(
self,
f: impl FnOnce(ResponseDataResult<T>) -> U,
) -> impl Future<Output = Result<U, U>>
where
T: DeserializeOwned + 'static,
U: 'static,
{
self.fetch_json(|fetch_object| f(fetch_object.response_data()))
}
async fn send_request(&self) -> Result<web_sys::Response, JsValue> {
let request_init = self.init_request_and_start_timeout();
let fetch_promise = web_sys::window()
.expect("fetch: cannot find window")
.fetch_with_str_and_init(&self.url, &request_init);
JsFuture::from(fetch_promise).await.map(Into::into)
}
fn init_request_and_start_timeout(&self) -> web_sys::RequestInit {
let mut init = web_sys::RequestInit::new();
let headers = web_sys::Headers::new().expect("fetch: cannot create headers");
for (name, value) in &self.headers {
headers
.append(name.as_str(), value.as_str())
.expect("fetch: cannot create header")
}
init.headers(&headers);
init.method(self.method.as_str());
if let Some(body) = &self.body {
init.body(Some(body));
}
if let Some(cache) = self.cache {
init.cache(cache);
}
if let Some(credentials) = self.credentials {
init.credentials(credentials);
}
if let Some(integrity) = &self.integrity {
init.integrity(integrity.as_str());
}
if let Some(mode) = self.mode {
init.mode(mode);
}
if let Some(redirect) = self.redirect {
init.redirect(redirect);
}
if let Some(referrer) = &self.referrer {
init.referrer(referrer.as_str());
}
if let Some(referrer_policy) = self.referrer_policy {
init.referrer_policy(referrer_policy);
}
if let Some(timeout) = &self.timeout {
let abort_controller = self.controller.clone();
*self.controller.timeout_handle.borrow_mut() = Some(
Timeout::new(*timeout, move || abort_controller.abort()),
);
}
init.signal(Some(&self.controller.abort_controller.signal()));
init
}
}