use std::time::Duration;
#[cfg(feature = "json")]
use serde::Serialize;
#[cfg(feature = "multipart")]
use crate::multipart::MultipartBuilder;
#[derive(Clone, Debug, Default)]
pub struct Headers {
pub headers: Vec<(String, String)>,
}
impl Headers {
pub fn new(headers: &[(&str, &str)]) -> Self {
Self {
headers: headers
.iter()
.map(|e| (e.0.to_owned(), e.1.to_owned()))
.collect(),
}
}
pub fn insert(&mut self, key: impl ToString, value: impl ToString) {
self.headers.push((key.to_string(), value.to_string()));
}
pub fn get(&self, key: &str) -> Option<&str> {
let key = key.to_string().to_lowercase();
self.headers
.iter()
.find(|(k, _)| k.to_lowercase() == key)
.map(|(_, v)| v.as_str())
}
pub fn get_all(&self, key: &str) -> impl Iterator<Item = &str> {
let key = key.to_string().to_lowercase();
self.headers
.iter()
.filter(move |(k, _)| k.to_lowercase() == key)
.map(|(_, v)| v.as_str())
}
pub fn sort(&mut self) {
self.headers.sort_by(|a, b| a.0.cmp(&b.0));
}
}
impl<const N: usize> From<&[(&str, &str); N]> for Headers {
fn from(headers: &[(&str, &str); N]) -> Self {
Self::new(headers.as_slice())
}
}
impl IntoIterator for Headers {
type Item = (String, String);
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.headers.into_iter()
}
}
impl<'h> IntoIterator for &'h Headers {
type Item = &'h (String, String);
type IntoIter = std::slice::Iter<'h, (String, String)>;
fn into_iter(self) -> Self::IntoIter {
self.headers.iter()
}
}
#[cfg(target_arch = "wasm32")]
#[derive(Default, Clone, Copy, Debug)]
pub enum Mode {
SameOrigin = 0,
NoCors = 1,
#[default]
Cors = 2,
Navigate = 3,
}
#[cfg(target_arch = "wasm32")]
impl From<Mode> for web_sys::RequestMode {
fn from(mode: Mode) -> Self {
match mode {
Mode::SameOrigin => web_sys::RequestMode::SameOrigin,
Mode::NoCors => web_sys::RequestMode::NoCors,
Mode::Cors => web_sys::RequestMode::Cors,
Mode::Navigate => web_sys::RequestMode::Navigate,
}
}
}
#[cfg(target_arch = "wasm32")]
#[derive(Default, Clone, Copy, Debug)]
pub enum Credentials {
#[default]
Omit = 0,
SameOrigin = 1,
Include = 2,
}
#[cfg(target_arch = "wasm32")]
impl From<Credentials> for web_sys::RequestCredentials {
fn from(credentials: Credentials) -> Self {
match credentials {
Credentials::Omit => web_sys::RequestCredentials::Omit,
Credentials::SameOrigin => web_sys::RequestCredentials::SameOrigin,
Credentials::Include => web_sys::RequestCredentials::Include,
}
}
}
#[derive(Clone, Debug)]
pub struct Request {
pub method: Method,
pub url: String,
pub body: Vec<u8>,
pub headers: Headers,
pub timeout: Option<Duration>,
#[cfg(target_arch = "wasm32")]
pub mode: Mode,
#[cfg(target_arch = "wasm32")]
pub credentials: Credentials,
}
impl Request {
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[expect(clippy::needless_pass_by_value)]
pub fn new(method: Method, url: impl ToString, headers: impl Into<Headers>) -> Self {
Self {
method,
url: url.to_string(),
body: vec![],
headers: headers.into(),
timeout: Some(Self::DEFAULT_TIMEOUT),
#[cfg(target_arch = "wasm32")]
mode: Mode::default(),
#[cfg(target_arch = "wasm32")]
credentials: Credentials::default(),
}
}
pub fn get(url: impl ToString) -> Self {
Self::new(Method::GET, url, &[("Accept", "*/*")])
}
pub fn head(url: impl ToString) -> Self {
Self::new(Method::HEAD, url, &[("Accept", "*/*")])
}
pub fn post(url: impl ToString, body: Vec<u8>) -> Self {
Self::new(
Method::POST,
url,
&[
("Accept", "*/*"),
("Content-Type", "text/plain; charset=utf-8"),
],
)
.with_body(body)
}
pub fn put(url: impl ToString, body: Vec<u8>) -> Self {
Self::new(
Method::PUT,
url,
&[
("Accept", "*/*"),
("Content-Type", "text/plain; charset=utf-8"),
],
)
.with_body(body)
}
pub fn delete(url: &str) -> Self {
Self::new(Method::DELETE, url, &[("Accept", "*/*")])
}
#[cfg(feature = "multipart")]
pub fn post_multipart(url: impl ToString, builder: MultipartBuilder) -> Self {
let (content_type, data) = builder.finish();
Self::new(
Method::POST,
url,
Headers::new(&[("Accept", "*/*"), ("Content-Type", content_type.as_str())]),
)
.with_body(data)
}
#[cfg(feature = "multipart")]
#[deprecated(note = "Renamed to `post_multipart`")]
pub fn multipart(url: impl ToString, builder: MultipartBuilder) -> Self {
Self::post_multipart(url, builder)
}
#[cfg(feature = "json")]
pub fn post_json<T>(url: impl ToString, body: &T) -> serde_json::error::Result<Self>
where
T: ?Sized + Serialize,
{
Ok(Self::new(
Method::POST,
url,
&[("Accept", "*/*"), ("Content-Type", "application/json")],
)
.with_body(serde_json::to_string(body)?.into_bytes()))
}
#[cfg(feature = "json")]
#[deprecated(note = "Renamed to `post_json`")]
pub fn json<T>(url: impl ToString, body: &T) -> serde_json::error::Result<Self>
where
T: ?Sized + Serialize,
{
Self::post_json(url, body)
}
#[cfg(feature = "json")]
pub fn put_json<T>(url: impl ToString, body: &T) -> serde_json::error::Result<Self>
where
T: ?Sized + Serialize,
{
Ok(Self::new(
Method::PUT,
url,
&[("Accept", "*/*"), ("Content-Type", "application/json")],
)
.with_body(serde_json::to_string(body)?.into_bytes()))
}
pub fn with_method(mut self, method: Method) -> Self {
self.method = method;
self
}
pub fn with_url(mut self, url: impl ToString) -> Self {
self.url = url.to_string();
self
}
pub fn with_body(mut self, body: Vec<u8>) -> Self {
self.body = body;
self
}
pub fn with_headers(mut self, headers: Headers) -> Self {
self.headers = headers;
self
}
pub fn with_header(mut self, key: impl ToString, value: impl ToString) -> Self {
self.headers.insert(key, value);
self
}
pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
self.timeout = timeout;
self
}
#[cfg(target_arch = "wasm32")]
pub fn with_mode(mut self, mode: Mode) -> Self {
self.mode = mode;
self
}
#[cfg(target_arch = "wasm32")]
pub fn with_credentials(mut self, credentials: Credentials) -> Self {
self.credentials = credentials;
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn fetch_raw_native(&self, with_timeout: bool) -> Result<ureq::http::Response<ureq::Body>> {
if self.method.contains_body() {
let mut req = match self.method {
Method::POST => ureq::post(&self.url),
Method::PATCH => ureq::patch(&self.url),
Method::PUT => ureq::put(&self.url),
_ => unreachable!(), };
for (k, v) in &self.headers {
req = req.header(k, v);
}
req = {
if with_timeout {
req.config()
} else {
req.config().timeout_recv_body(self.timeout)
}
.http_status_as_error(false)
.build()
};
if self.body.is_empty() {
req.send_empty()
} else {
req.send(&self.body)
}
} else {
let mut req = match self.method {
Method::GET => ureq::get(&self.url),
Method::DELETE => ureq::delete(&self.url),
Method::CONNECT => ureq::connect(&self.url),
Method::HEAD => ureq::head(&self.url),
Method::OPTIONS => ureq::options(&self.url),
Method::TRACE => ureq::trace(&self.url),
Method::PATCH | Method::POST | Method::PUT => unreachable!(), };
req = req
.config()
.timeout_recv_body(self.timeout)
.http_status_as_error(false)
.build();
for (k, v) in &self.headers {
req = req.header(k, v);
}
if self.body.is_empty() {
req.call()
} else {
req.force_send_body().send(&self.body)
}
}
.map_err(|err| err.to_string())
}
}
#[derive(Clone)]
pub struct Response {
pub url: String,
pub ok: bool,
pub status: u16,
pub status_text: String,
pub headers: Headers,
pub bytes: Vec<u8>,
}
impl Response {
pub fn text(&self) -> Option<&str> {
std::str::from_utf8(&self.bytes).ok()
}
#[cfg(feature = "json")]
pub fn json<T: serde::de::DeserializeOwned>(&self) -> serde_json::Result<T> {
serde_json::from_slice(self.bytes.as_slice())
}
pub fn content_type(&self) -> Option<&str> {
self.headers.get("content-type")
}
}
impl std::fmt::Debug for Response {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
url,
ok,
status,
status_text,
headers,
bytes,
} = self;
fmt.debug_struct("Response")
.field("url", url)
.field("ok", ok)
.field("status", status)
.field("status_text", status_text)
.field("headers", headers)
.field("bytes", &format!("{} bytes", bytes.len()))
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct PartialResponse {
pub url: String,
pub ok: bool,
pub status: u16,
pub status_text: String,
pub headers: Headers,
}
impl PartialResponse {
pub fn complete(self, bytes: Vec<u8>) -> Response {
let Self {
url,
ok,
status,
status_text,
headers,
} = self;
Response {
url,
ok,
status,
status_text,
headers,
bytes,
}
}
}
pub type Error = String;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Method {
GET,
HEAD,
POST,
PUT,
DELETE,
CONNECT,
OPTIONS,
TRACE,
PATCH,
}
impl Method {
pub fn contains_body(&self) -> bool {
use Method::*;
match self {
POST | PATCH | PUT => true,
_ => false,
}
}
pub fn parse(string: &str) -> Result<Self> {
use Method::*;
match string {
"GET" => Ok(GET),
"HEAD" => Ok(HEAD),
"POST" => Ok(POST),
"PUT" => Ok(PUT),
"DELETE" => Ok(DELETE),
"CONNECT" => Ok(CONNECT),
"OPTIONS" => Ok(OPTIONS),
"TRACE" => Ok(TRACE),
"PATCH" => Ok(PATCH),
_ => Err(Error::from("Failed to parse HTTP method")),
}
}
pub fn as_str(&self) -> &'static str {
use Method::*;
match self {
GET => "GET",
HEAD => "HEAD",
POST => "POST",
PUT => "PUT",
DELETE => "DELETE",
CONNECT => "CONNECT",
OPTIONS => "OPTIONS",
TRACE => "TRACE",
PATCH => "PATCH",
}
}
}