seed 0.9.2

A Rust framework for creating web apps, using WebAssembly
Documentation
//! The Request of the Fetch API.

use super::form_data::FormData;
use super::{fetch, FetchError, Header, Headers, Method, Response, Result};
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
use crate::browser::json;
use crate::browser::Url;
use gloo_timers::callback::Timeout;
use js_sys::Uint8Array;
#[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
use serde::Serialize;
use std::{borrow::Cow, cell::RefCell, rc::Rc};
use wasm_bindgen::JsValue;

/// Its methods configure the request, and handle the response. Many of them return the original
/// struct, and are intended to be used chained together.
#[derive(Debug, Clone, Default)]
pub struct Request<'a> {
    url: Cow<'a, str>,
    headers: Headers<'a>,
    method: Method,
    body: Option<Cow<'a, 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<'a> Request<'a> {
    /// Create new request based on the provided url.
    ///
    /// To get a [`Response`](./struct.Response.html) you need to pass
    /// `Request` to the [`fetch`](./fn.fetch.html) function.
    ///
    /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Request)
    #[must_use]
    pub fn new(url: impl Into<Cow<'a, str>>) -> Self {
        Self {
            url: url.into(),
            ..Self::default()
        }
    }

    // TODO: remove when https://github.com/rust-lang/rust-clippy/issues/4979 will be fixed
    #[allow(clippy::missing_const_for_fn)]
    /// Set headers for this request.
    /// It will replace any existing headers.
    #[must_use]
    pub fn headers(mut self, headers: Headers<'a>) -> Self {
        self.headers = headers;
        self
    }

    /// Set specific header.
    #[must_use]
    pub fn header(mut self, header: Header<'a>) -> Self {
        self.headers.set(header);
        self
    }

    /// Set HTTP method. Default method is `GET`.
    #[must_use]
    pub const fn method(mut self, method: Method) -> Self {
        self.method = method;
        self
    }

    /// Set request body to provided `JsValue`. Consider using `json`, `text`
    /// or `bytes` methods instead.
    ///
    /// ## Panics
    /// This method will panic when request method is GET or HEAD.
    #[must_use]
    pub fn body(mut self, body: JsValue) -> Self {
        self.body = Some(Cow::Owned(body));

        #[cfg(debug_assertions)]
        match self.method {
            Method::Get | Method::Head => {
                error!("GET and HEAD requests shoudn't have a body");
            }
            _ => {}
        }

        self
    }

    /// Set request body to provided `JsValue` by reference. Consider using
    /// `json`, `text` or `bytes` methods instead.
    ///
    /// ## Panics
    /// This method will panic when request method is GET or HEAD.
    #[must_use]
    pub fn body_ref(mut self, body: &'a JsValue) -> Self {
        self.body = Some(Cow::Borrowed(body));

        #[cfg(debug_assertions)]
        match self.method {
            Method::Get | Method::Head => {
                error!("GET and HEAD requests shoudn't have a body");
            }
            _ => {}
        }

        self
    }

    /// Set request body by JSON encoding provided data.
    /// It will also set `Content-Type` header to `application/json; charset=utf-8`.
    ///
    /// # Errors
    ///
    /// This method can fail if JSON serialization fail. It will then
    /// return `FetchError::SerdeError`.
    #[cfg(any(feature = "serde-json", feature = "serde-wasm-bindgen"))]
    pub fn json<T: Serialize + ?Sized>(mut self, data: &T) -> Result<Self> {
        let body = json::to_js_string(data)?;
        self.body = Some(Cow::Owned(body.into()));
        Ok(self.header(Header::content_type("application/json; charset=utf-8")))
    }

    /// Set request body to a provided string.
    /// It will also set `Content-Type` header to `text/plain; charset=utf-8`.
    #[must_use]
    pub fn text(mut self, text: impl AsRef<str>) -> Self {
        self.body = Some(Cow::Owned(JsValue::from(text.as_ref())));
        self.header(Header::content_type("text/plain; charset=utf-8"))
    }

    /// Set request body to the provided bytes.
    /// It will also set `Content-Type` header to `application/octet-stream`.
    #[must_use]
    pub fn bytes(mut self, bytes: impl AsRef<[u8]>) -> Self {
        self.body = Some(Cow::Owned(Uint8Array::from(bytes.as_ref()).into()));
        self.header(Header::content_type("application/octet-stream"))
    }

    /// Set request body to the provided form data object.
    /// It will also set `Content-Type` header to `multipart/form-data`.
    #[must_use]
    pub fn form_data(mut self, form_data: FormData) -> Self {
        self.body = Some(Cow::Owned(form_data.into()));
        self
    }

    /// Set request mode.
    ///
    /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Request/mode)
    #[must_use]
    pub const fn mode(mut self, mode: web_sys::RequestMode) -> Self {
        self.mode = Some(mode);
        self
    }

    /// Set request credentials.
    ///
    /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
    #[must_use]
    pub const fn credentials(mut self, credentials: web_sys::RequestCredentials) -> Self {
        self.credentials = Some(credentials);
        self
    }

    /// Set request cache mode.
    ///
    /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Request/cache)
    #[must_use]
    pub const fn cache(mut self, cache: web_sys::RequestCache) -> Self {
        self.cache = Some(cache);
        self
    }

    /// Set request redirect mode.
    ///
    /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Request/redirect)
    #[must_use]
    pub const fn redirect(mut self, redirect: web_sys::RequestRedirect) -> Self {
        self.redirect = Some(redirect);
        self
    }

    /// Set request referrer.
    ///
    /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Request/referrer)
    #[must_use]
    pub fn referrer(mut self, referrer: &impl ToString) -> Self {
        self.referrer = Some(referrer.to_string());
        self
    }

    /// Set request referrer policy.
    ///
    /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Request/referrerPolicy)
    #[must_use]
    pub const fn referrer_policy(mut self, referrer_policy: web_sys::ReferrerPolicy) -> Self {
        self.referrer_policy = Some(referrer_policy);
        self
    }

    /// Set request subresource integrity.
    ///
    /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Request/integrity)
    #[must_use]
    pub fn integrity(mut self, integrity: &impl ToString) -> Self {
        self.integrity = Some(integrity.to_string());
        self
    }

    /// Set request timeout in milliseconds.
    #[must_use]
    pub const fn timeout(mut self, timeout: u32) -> Self {
        self.timeout = Some(timeout);
        self
    }

    /// Get the request controller that allows to abort request or disable request's timeout.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// let (request, controller) = Request::new("http://example.com").controller();
    /// ```
    pub fn controller(self) -> (Self, RequestController) {
        let controller = self.controller.clone();
        (self, controller)
    }

    /// Fetch request. It's a chainable alternative to `fetch(request)`.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// orders.perform_cmd({
    ///     let message = model.new_message.clone();
    ///     async { Msg::Fetched(send_message(message).await) }
    /// });
    /// ...
    /// async fn send_message(new_message: String) -> fetch::Result<shared::SendMessageResponseBody> {
    ///     Request::new(get_request_url())
    ///         .method(Method::Post)
    ///         .json(&shared::SendMessageRequestBody { text: new_message })?
    ///         .fetch()
    ///         .await?
    ///         .check_status()?
    ///         .json()
    ///         .await
    /// }
    /// ```
    ///
    /// ## Errors
    ///
    /// `fetch` will return `Err` only on network errors. This means that
    /// even if you get `Ok` from this function, you still need to check
    /// `Response` status for HTTP errors.
    pub async fn fetch(self) -> Result<Response> {
        fetch(self).await
    }
}

impl<'a, T: Into<Cow<'a, str>>> From<T> for Request<'a> {
    fn from(s: T) -> Request<'a> {
        Request::new(s)
    }
}

impl<'a> From<Url> for Request<'a> {
    fn from(url: Url) -> Request<'a> {
        Request::new(url.to_string())
    }
}

impl TryFrom<Request<'_>> for web_sys::Request {
    type Error = FetchError;
    fn try_from(request: Request) -> std::result::Result<Self, Self::Error> {
        let mut init = web_sys::RequestInit::new();

        // headers
        let headers = web_sys::Headers::new().map_err(FetchError::RequestError)?;
        for header in request.headers {
            headers
                .append(&header.name, &header.value)
                .map_err(FetchError::RequestError)?;
        }
        init.headers(&headers);

        // method
        init.method(request.method.as_str());

        // body
        if let Some(body) = request.body {
            init.body(Some(&body));
        }

        // cache
        if let Some(cache) = request.cache {
            init.cache(cache);
        }

        // credentials
        if let Some(credentials) = request.credentials {
            init.credentials(credentials);
        }

        // integrity
        if let Some(integrity) = &request.integrity {
            init.integrity(integrity.as_str());
        }

        // mode
        if let Some(mode) = request.mode {
            init.mode(mode);
        }

        // redirect
        if let Some(redirect) = request.redirect {
            init.redirect(redirect);
        }

        // referrer
        if let Some(referrer) = &request.referrer {
            init.referrer(referrer.as_str());
        }

        // referrer_policy
        if let Some(referrer_policy) = request.referrer_policy {
            init.referrer_policy(referrer_policy);
        }

        // timeout
        if let Some(timeout) = &request.timeout {
            let abort_controller = request.controller.clone();
            request.controller.timeout_handle.replace(Some(
                // abort request on timeout
                Timeout::new(*timeout, move || abort_controller.abort()),
            ));
        }

        // controller
        // https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
        init.signal(Some(&request.controller.abort_controller.signal()));

        // It seems that the only reason why Request constructor can
        // fail is when Url contains credentials.  I assume that this
        // use case should be extremely rare, so to make api a bit
        // simplier let's just unwrap it here.
        //
        // See https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#Errors
        web_sys::Request::new_with_str_and_init(&request.url, &init)
            .map_err(FetchError::RequestError)
    }
}

#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone)]
/// It allows to abort request or disable request's timeout.
/// You can get it by calling method `Request.controller`.
pub struct RequestController {
    abort_controller: Rc<web_sys::AbortController>,
    timeout_handle: Rc<RefCell<Option<Timeout>>>,
}

impl RequestController {
    /// Abort request and disable request's timeout.
    ///
    /// [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort)
    pub fn abort(&self) {
        // Cancel timeout by dropping it.
        self.timeout_handle.replace(None);
        self.abort_controller.abort();
    }
    /// Disable request's timeout.
    ///
    /// # Errors
    ///
    /// Will return error if timeout is already disabled.
    pub fn disable_timeout(&self) -> std::result::Result<(), &'static str> {
        // Cancel timeout by dropping it.
        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)),
        }
    }
}

#[cfg(test)]
mod tests {
    use wasm_bindgen::JsCast;
    use wasm_bindgen_test::*;
    wasm_bindgen_test_configure!(run_in_browser);
    use super::*;

    #[wasm_bindgen_test]
    async fn request_bytes() {
        let request = Request::new("").bytes([6, 2, 8, 3, 1, 8]);
        assert_eq!(
            request
                .body
                .unwrap()
                .dyn_ref::<Uint8Array>()
                .unwrap()
                .to_vec(),
            Vec::from([6, 2, 8, 3, 1, 8])
        )
    }
}