use std::{collections::HashMap, fmt::Debug, future::Future, str::FromStr, sync::Arc};
use bytes::Bytes;
#[cfg(feature = "http3")]
use h3_quinn::OpenStreams;
use http::{
header::{self},
HeaderMap, HeaderName, HeaderValue, Method,
};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use http_body_util::combinators::BoxBody;
use hyper_body_utils::HttpBody;
use log::error;
use regex::Regex;
use serde::Serialize;
use url::Url;
use crate::{
client::serde::RequestBody,
cookie::DeboaCookie,
errors::{DeboaError, RequestError},
form::{DeboaForm, Form},
response::DeboaResponse,
url::IntoUrl,
Client, Result,
};
pub type BytesBody = BoxBody<Bytes, std::io::Error>;
#[cfg(feature = "smol-rt")]
pub type File = smol::fs::File;
#[cfg(feature = "tokio-rt")]
pub type File = tokio::fs::File;
#[cfg(feature = "compio-rt")]
pub type File = compio_fs::File;
#[cfg(feature = "http1")]
pub type Http1Request = hyper::client::conn::http1::SendRequest<HttpBody>;
#[cfg(feature = "http2")]
pub type Http2Request = hyper::client::conn::http2::SendRequest<HttpBody>;
#[cfg(feature = "http3")]
pub type Http3Request = h3::client::SendRequest<OpenStreams, Bytes>;
pub trait IntoRequest: private::IntoRequestSealed {
fn into_request(self) -> Result<DeboaRequest>;
}
impl IntoRequest for DeboaRequest {
#[inline]
fn into_request(self) -> Result<DeboaRequest> {
Ok(self)
}
}
impl IntoRequest for &str {
#[inline]
fn into_request(self) -> Result<DeboaRequest> {
DeboaRequest::get(self)?.build()
}
}
impl IntoRequest for String {
#[inline]
fn into_request(self) -> Result<DeboaRequest> {
DeboaRequest::get(self)?.build()
}
}
impl IntoRequest for Url {
#[inline]
fn into_request(self) -> Result<DeboaRequest> {
DeboaRequest::get(self)?.build()
}
}
pub trait IntoHeaders: private::IntoHeadersSealed {
fn into_headers(self) -> Result<HeaderMap>;
}
impl IntoHeaders for HeaderMap {
#[inline]
fn into_headers(self) -> Result<HeaderMap> {
Ok(self)
}
}
impl IntoHeaders for Vec<(HeaderName, String)> {
#[inline]
fn into_headers(self) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
for (key, value) in self {
headers.insert(&key, HeaderValue::from_str(&value).expect("Invalid header value"));
}
Ok(headers)
}
}
impl IntoHeaders for Vec<(String, String)> {
#[inline]
fn into_headers(self) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
for (key, value) in self {
headers.insert(
HeaderName::from_str(&key).expect("Invalid header name"),
HeaderValue::from_str(&value).expect("Invalid header value"),
);
}
Ok(headers)
}
}
impl<'a> IntoHeaders for Vec<(&'a str, &'a str)> {
#[inline]
fn into_headers(self) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
for (key, value) in self {
headers.insert(
HeaderName::from_str(key).expect("Invalid header name"),
HeaderValue::from_str(value).expect("Invalid header value"),
);
}
Ok(headers)
}
}
pub trait MethodExt: private::MethodExtSealed {
fn from_url(self, url: &str) -> Result<DeboaRequestBuilder>;
fn to_url(self, url: &str) -> Result<DeboaRequestBuilder>;
}
impl MethodExt for Method {
fn from_url(self, url: &str) -> Result<DeboaRequestBuilder> {
match self {
Method::GET => DeboaRequest::get(url),
Method::POST => DeboaRequest::post(url),
Method::PUT => DeboaRequest::put(url),
Method::DELETE => DeboaRequest::delete(url),
Method::PATCH => DeboaRequest::patch(url),
_ => panic!("Method not supported"),
}
}
fn to_url(self, url: &str) -> Result<DeboaRequestBuilder> {
self.from_url(url)
}
}
impl MethodExt for &str {
#[inline]
fn from_url(self, url: &str) -> Result<DeboaRequestBuilder> {
match self {
"GET" | "get" => DeboaRequest::get(url),
"POST" | "post" => DeboaRequest::post(url),
"PUT" | "put" => DeboaRequest::put(url),
"DELETE" | "delete" => DeboaRequest::delete(url),
"PATCH" | "patch" => DeboaRequest::patch(url),
_ => panic!("Method not supported"),
}
}
#[inline]
fn to_url(self, url: &str) -> Result<DeboaRequestBuilder> {
self.from_url(url)
}
}
#[deprecated(note = "Use FetchWith trait instead", since = "0.0.8")]
pub trait Fetch {
fn fetch<T>(&self, client: T) -> impl Future<Output = Result<DeboaResponse>>
where
T: AsRef<Client> + Send;
}
#[allow(deprecated)]
impl Fetch for &str {
#[inline]
async fn fetch<T>(&self, client: T) -> Result<DeboaResponse>
where
T: AsRef<Client> + Send,
{
DeboaRequest::get(*self)?
.send_with(client)
.await
}
}
pub trait FetchWith {
fn fetch_with<T>(&self, client: T) -> impl Future<Output = Result<DeboaResponse>>
where
T: AsRef<Client> + Send;
}
impl FetchWith for &str {
#[inline]
async fn fetch_with<T>(&self, client: T) -> Result<DeboaResponse>
where
T: AsRef<Client> + Send,
{
DeboaRequest::get(*self)?
.send_with(client)
.await
}
}
impl FetchWith for String {
#[inline]
async fn fetch_with<T>(&self, client: T) -> Result<DeboaResponse>
where
T: AsRef<Client> + Send,
{
DeboaRequest::get(self)?
.send_with(client)
.await
}
}
#[inline]
pub fn get<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
DeboaRequest::get(url)
}
#[inline]
pub fn post<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
DeboaRequest::post(url)
}
#[inline]
pub fn put<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
DeboaRequest::put(url)
}
#[inline]
pub fn delete<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
DeboaRequest::delete(url)
}
#[inline]
pub fn patch<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
DeboaRequest::patch(url)
}
pub struct DeboaRequestBuilder {
retries: u32,
url: Arc<Url>,
headers: HeaderMap,
cookies: Option<HashMap<String, DeboaCookie>>,
method: http::Method,
body: HttpBody,
form: Option<Form>,
}
impl DeboaRequestBuilder {
#[inline]
pub fn retries(mut self, retries: u32) -> Self {
self.retries = retries;
self
}
#[inline]
pub fn method(mut self, method: http::Method) -> Self {
self.method = method;
self
}
#[inline]
pub fn file(mut self, file: File) -> Self {
self.body = HttpBody::from_file(file);
self
}
#[inline]
pub fn bytes(mut self, body: &[u8]) -> Self {
self.body = HttpBody::from_bytes(body);
self
}
#[inline]
pub fn body(mut self, body: HttpBody) -> Self {
self.body = body;
self
}
#[inline]
pub fn headers<I>(mut self, headers: I) -> Self
where
I: IntoHeaders,
{
self.headers = headers
.into_headers()
.unwrap_or_default();
self
}
#[inline]
pub fn header(mut self, key: HeaderName, value: &str) -> Self {
self.headers
.insert(key, HeaderValue::from_str(value).unwrap());
self
}
#[inline]
pub fn cookies(mut self, cookies: HashMap<String, DeboaCookie>) -> Self {
self.cookies = Some(cookies);
self
}
#[inline]
pub fn cookie(mut self, cookie: DeboaCookie) -> Self {
if let Some(cookies) = &mut self.cookies {
cookies.insert(
cookie
.name()
.to_string(),
cookie,
);
} else {
self.cookies = Some(HashMap::from([(
cookie
.name()
.to_string(),
cookie,
)]));
}
self
}
#[inline]
pub fn form(mut self, form: Form) -> Self {
self.form = Some(form);
self
}
#[inline]
pub fn text(mut self, text: &str) -> Self {
self.body = HttpBody::from_bytes(text.as_bytes());
self
}
#[inline]
pub fn body_as<T: RequestBody, B: Serialize>(self, body_type: T, body: B) -> Result<Self> {
Ok(self
.header(header::CONTENT_TYPE, body_type.mime_type())
.header(header::ACCEPT, body_type.mime_type())
.body(HttpBody::from_bytes(&body_type.serialize(body)?)))
}
#[inline]
pub fn bearer_auth(self, token: &str) -> Self {
self.header(header::AUTHORIZATION, format!("Bearer {token}").as_str())
}
#[inline]
pub fn basic_auth(self, username: &str, password: &str) -> Self {
self.header(
header::AUTHORIZATION,
format!("Basic {}", STANDARD.encode(format!("{username}:{password}"))).as_str(),
)
}
#[inline]
pub fn build(self) -> Result<DeboaRequest> {
let mut request = DeboaRequest {
url: self.url,
headers: self.headers,
cookies: self.cookies,
retries: self.retries,
method: self.method,
body: self.body,
};
if let Some(host) = request.url().host() {
match HeaderValue::from_str(
host.to_string()
.as_str(),
) {
Ok(value) => {
request
.headers_mut()
.insert(header::HOST, value);
}
Err(err) => return Err(DeboaError::Header { message: err.to_string() }),
}
}
if let Some(form) = self.form {
let (content_type, body) = match form {
Form::EncodedForm(form) => (form.content_type(), form.build()),
Form::MultiPartForm(form) => (form.content_type(), form.build()),
};
match HeaderValue::from_str(content_type.as_str()) {
Ok(value) => {
request
.headers_mut()
.insert(header::CONTENT_TYPE, value);
}
Err(err) => return Err(DeboaError::Header { message: err.to_string() }),
}
request.body = HttpBody::from_bytes(&body);
}
Ok(request)
}
#[deprecated(note = "Use `send_with` method instead", since = "0.0.8")]
#[inline]
pub async fn go<T>(self, client: T) -> Result<DeboaResponse>
where
T: AsRef<Client>,
{
client
.as_ref()
.execute(self.build()?)
.await
}
#[inline]
pub async fn send_with<T>(self, client: T) -> Result<DeboaResponse>
where
T: AsRef<Client>,
{
client
.as_ref()
.execute(self.build()?)
.await
}
}
pub struct DeboaRequest {
url: Arc<Url>,
headers: HeaderMap,
cookies: Option<HashMap<String, DeboaCookie>>,
retries: u32,
method: http::Method,
body: HttpBody,
}
impl Debug for DeboaRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeboaRequest")
.field("url", &self.url)
.field("headers", &self.headers)
.field("cookies", &self.cookies)
.field("retries", &self.retries)
.field("method", &self.method)
.finish()
}
}
impl FromStr for DeboaRequest {
type Err = DeboaError;
fn from_str(s: &str) -> Result<Self> {
let lines = s.lines();
let mut headers = HeaderMap::new();
let mut url = String::new();
let mut method = String::new();
let mut body = Vec::new();
let mut is_reading_body = false;
let method_url_regex =
Regex::new(r"(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(https?://[^\s]+)");
if let Err(e) = method_url_regex {
error!("Failed to parse request: {}", e);
return Err(DeboaError::Request(RequestError::Parse { message: e.to_string() }));
}
for line in lines {
let line = line.trim();
if !is_reading_body {
let regex = method_url_regex
.as_ref()
.unwrap();
let captures = regex.captures(line);
if let Some(captures) = captures {
let method_cap = captures.get(1);
if method_cap.is_none() {
error!("Missing method in request format");
return Err(DeboaError::Request(RequestError::Parse {
message: "Missing method in request format".into(),
}));
}
let url_cap = captures.get(2);
if url_cap.is_none() {
error!("Missing url in request format");
return Err(DeboaError::Request(RequestError::Parse {
message: "Missing url in request format".into(),
}));
}
method = method_cap
.unwrap()
.as_str()
.to_string();
url = url_cap
.unwrap()
.as_str()
.to_string();
continue;
}
let header = line.split_once(':');
if let Some(header) = header {
let header_name = HeaderName::from_bytes(
header
.0
.trim()
.as_bytes(),
)
.map_err(|_| {
error!("Invalid header name");
DeboaError::Request(RequestError::Parse {
message: "Invalid header name".into(),
})
})?;
let header_value = HeaderValue::from_bytes(
header
.1
.trim()
.as_bytes(),
)
.map_err(|_| {
error!("Invalid header value");
DeboaError::Request(RequestError::Parse {
message: "Invalid header value".into(),
})
})?;
headers.insert(header_name, header_value);
continue;
}
}
if line.is_empty() && !url.is_empty() && !headers.is_empty() {
is_reading_body = true;
continue;
}
if is_reading_body {
body.extend_from_slice(line.as_bytes());
}
}
let url = url.parse_url()?;
if headers
.get(header::HOST)
.is_none()
{
let authority = url.authority();
headers.insert(header::HOST, HeaderValue::from_str(authority).unwrap());
}
Ok(DeboaRequest {
url: Arc::new(url),
headers,
cookies: None,
retries: 0,
method: method
.parse::<http::Method>()
.unwrap(),
body: HttpBody::from_bytes(&body),
})
}
}
impl AsRef<DeboaRequest> for DeboaRequest {
fn as_ref(&self) -> &DeboaRequest {
self
}
}
impl AsMut<DeboaRequest> for DeboaRequest {
fn as_mut(&mut self) -> &mut DeboaRequest {
self
}
}
impl DeboaRequest {
#[inline]
pub fn at<T: IntoUrl>(url: T, method: http::Method) -> Result<DeboaRequestBuilder> {
let parsed_url = url.into_url();
if let Err(e) = parsed_url {
error!("Failed to parse url: {}", e);
return Err(DeboaError::Request(RequestError::UrlParse { message: e.to_string() }));
}
let url = parsed_url.unwrap();
let authority = url.authority();
let mut headers = HeaderMap::new();
headers.insert(header::HOST, HeaderValue::from_str(authority).unwrap());
Ok(DeboaRequestBuilder {
url: url.into(),
headers,
cookies: None,
retries: 0,
method,
body: HttpBody::from_bytes(&[]),
form: None,
})
}
#[inline]
pub fn from<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
DeboaRequest::at(url, Method::GET)
}
#[inline]
pub fn to<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
DeboaRequest::at(url, Method::POST)
}
#[inline]
pub fn get<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
Ok(DeboaRequest::from(url)?.method(Method::GET))
}
#[inline]
pub fn post<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
Ok(DeboaRequest::to(url)?.method(Method::POST))
}
#[inline]
pub fn put<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
Ok(DeboaRequest::to(url)?.method(Method::PUT))
}
#[inline]
pub fn patch<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
Ok(DeboaRequest::to(url)?.method(Method::PATCH))
}
#[inline]
pub fn delete<T: IntoUrl>(url: T) -> Result<DeboaRequestBuilder> {
Ok(DeboaRequest::from(url)?.method(Method::DELETE))
}
#[inline]
pub fn method(&self) -> &http::Method {
&self.method
}
#[inline]
pub fn url(&self) -> Arc<Url> {
Arc::clone(&self.url)
}
#[inline]
pub fn retries(&self) -> u32 {
self.retries
}
#[inline]
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
#[inline]
pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers
}
#[inline]
pub fn cookies(&self) -> Option<&HashMap<String, DeboaCookie>> {
self.cookies
.as_ref()
}
pub fn body(self) -> HttpBody {
self.body
}
}
mod private {
pub trait IntoRequestSealed {}
pub trait IntoHeadersSealed {}
pub trait MethodExtSealed {}
}
impl private::IntoRequestSealed for DeboaRequest {}
impl private::IntoRequestSealed for &str {}
impl private::IntoRequestSealed for String {}
impl private::IntoRequestSealed for Url {}
impl private::IntoHeadersSealed for HeaderMap {}
impl private::IntoHeadersSealed for Vec<(HeaderName, String)> {}
impl private::IntoHeadersSealed for Vec<(String, String)> {}
impl<'a> private::IntoHeadersSealed for Vec<(&'a str, &'a str)> {}
impl private::MethodExtSealed for Method {}
impl private::MethodExtSealed for &str {}