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 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()
}
}
#[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,
}
}
}
#[derive(Clone, Debug)]
pub struct Request {
pub method: String,
pub url: String,
pub body: Vec<u8>,
pub headers: Headers,
pub mode: Mode,
pub timeout: Option<Duration>,
}
impl Request {
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[allow(clippy::needless_pass_by_value)]
pub fn get(url: impl ToString) -> Self {
Self {
method: "GET".to_owned(),
url: url.to_string(),
body: vec![],
headers: Headers::new(&[("Accept", "*/*")]),
mode: Mode::default(),
timeout: Some(Self::DEFAULT_TIMEOUT),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn head(url: impl ToString) -> Self {
Self {
method: "HEAD".to_owned(),
url: url.to_string(),
body: vec![],
headers: Headers::new(&[("Accept", "*/*")]),
mode: Mode::default(),
timeout: Some(Self::DEFAULT_TIMEOUT),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn post(url: impl ToString, body: Vec<u8>) -> Self {
Self {
method: "POST".to_owned(),
url: url.to_string(),
body,
headers: Headers::new(&[
("Accept", "*/*"),
("Content-Type", "text/plain; charset=utf-8"),
]),
mode: Mode::default(),
timeout: Some(Self::DEFAULT_TIMEOUT),
}
}
#[cfg(feature = "multipart")]
pub fn multipart(url: impl ToString, builder: MultipartBuilder) -> Self {
let (content_type, data) = builder.finish();
Self {
method: "POST".to_string(),
url: url.to_string(),
body: data,
headers: Headers::new(&[("Accept", "*/*"), ("Content-Type", content_type.as_str())]),
mode: Mode::default(),
timeout: Some(Self::DEFAULT_TIMEOUT),
}
}
#[cfg(feature = "json")]
#[allow(clippy::needless_pass_by_value)]
pub fn json<T>(url: impl ToString, body: &T) -> serde_json::error::Result<Self>
where
T: ?Sized + Serialize,
{
Ok(Self {
method: "POST".to_owned(),
url: url.to_string(),
body: serde_json::to_string(body)?.into_bytes(),
headers: Headers::new(&[("Accept", "*/*"), ("Content-Type", "application/json")]),
mode: Mode::default(),
timeout: Some(Self::DEFAULT_TIMEOUT),
})
}
pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
self.timeout = timeout;
self
}
}
#[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>;