rikka 0.2.0

Async HTTP client for native & WASM.
Documentation
use crate::error::Result;
use crate::response::Response;

/// HTTP request method.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Method {
    #[default]
    Get,
    Post,
    Put,
    Patch,
    Delete,
    Head,
    Options,
}

impl Method {
    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Get => "GET",
            Self::Post => "POST",
            Self::Put => "PUT",
            Self::Patch => "PATCH",
            Self::Delete => "DELETE",
            Self::Head => "HEAD",
            Self::Options => "OPTIONS",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Redirect {
    /// Follow redirects automatically.
    #[default]
    Follow,
    /// Do not follow redirects; expose the 3xx response with its `Location`
    /// and `Set-Cookie` headers.
    ///
    /// On WASM this needs a runtime whose `fetch` returns the real 3xx in
    /// `manual` redirect mode (Node, Bun, Deno). Browsers return an opaque
    /// redirect instead: status 0 and no readable headers.
    Manual,
}

/// A request description, built with [`Request::builder`] then sent.
#[derive(Debug, Clone)]
pub struct Request {
    pub(crate) method: Method,
    pub(crate) url: String,
    pub(crate) headers: Vec<(String, String)>,
    pub(crate) cookies: Vec<(String, String)>,
    /// `application/x-www-form-urlencoded` body, if any.
    pub(crate) form: Option<Vec<(String, String)>>,
    /// Serialized `application/json` body, if any. Takes precedence over
    /// `form` when both are set.
    pub(crate) json: Option<String>,
    pub(crate) redirect: Redirect,
    // never read on wasm. tls is controlled by the host fetch there.
    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
    pub(crate) unauthorized_tls: bool,
}

impl Request {
    #[must_use]
    pub fn builder(url: impl Into<String>) -> RequestBuilder {
        RequestBuilder {
            inner: Request {
                method: Method::Get,
                url: url.into(),
                headers: Vec::new(),
                cookies: Vec::new(),
                form: None,
                json: None,
                redirect: Redirect::Follow,
                unauthorized_tls: false,
            },
        }
    }

    /// The `Cookie` header line for the requested cookies, if any.
    pub(crate) fn cookie_header(&self) -> Option<String> {
        if self.cookies.is_empty() {
            return None;
        }

        Some(
            self.cookies
                .iter()
                .map(|(k, v)| format!("{k}={v}"))
                .collect::<Vec<_>>()
                .join("; "),
        )
    }

    /// Whether the caller already set a `Content-Type` header.
    pub(crate) fn has_content_type(&self) -> bool {
        self.headers
            .iter()
            .any(|(key, _)| key.eq_ignore_ascii_case("content-type"))
    }
}

pub struct RequestBuilder {
    inner: Request,
}

impl RequestBuilder {
    #[must_use]
    pub fn method(mut self, method: Method) -> Self {
        self.inner.method = method;
        self
    }

    #[must_use]
    pub fn post(self) -> Self {
        self.method(Method::Post)
    }

    #[must_use]
    pub fn put(self) -> Self {
        self.method(Method::Put)
    }

    #[must_use]
    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.inner.headers.push((key.into(), value.into()));
        self
    }

    #[must_use]
    pub fn cookie(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.inner.cookies.push((key.into(), value.into()));
        self
    }

    /// Set an `application/x-www-form-urlencoded` body.
    #[must_use]
    pub fn form(mut self, body: Vec<(String, String)>) -> Self {
        self.inner.form = Some(body);
        self
    }

    /// Set an `application/json` body serialized from `body`. Takes
    /// precedence over [`form`](Self::form) when both are set.
    #[cfg(feature = "json")]
    pub fn json(mut self, body: &impl serde::Serialize) -> Result<Self> {
        self.inner.json = Some(serde_json::to_string(body)?);
        Ok(self)
    }

    #[must_use]
    pub fn redirect(mut self, redirect: Redirect) -> Self {
        self.inner.redirect = redirect;
        self
    }

    /// Accept invalid TLS certificates - self-signed, expired, ... - on
    /// native. On WASM, TLS is controlled by the host `fetch` and this is a
    /// no-op.
    #[must_use]
    pub fn unauthorized_tls(mut self) -> Self {
        self.inner.unauthorized_tls = true;
        self
    }

    #[must_use]
    pub fn build(self) -> Request {
        self.inner
    }

    pub async fn send(self) -> Result<Response> {
        crate::send(self.build()).await
    }
}