use std::{collections::HashMap, fmt::Debug, str::FromStr, sync::Arc};
use async_trait::async_trait;
use http::{
header::{self, HOST},
HeaderMap, HeaderName, HeaderValue, Method,
};
use base64::{engine::general_purpose::STANDARD, Engine as _};
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,
Deboa, Result,
};
#[async_trait]
pub trait IntoRequest: private::Sealed {
fn into_request(self) -> Result<DeboaRequest>;
}
impl IntoRequest for DeboaRequest {
fn into_request(self) -> Result<DeboaRequest> {
Ok(self)
}
}
impl IntoRequest for &str {
fn into_request(self) -> Result<DeboaRequest> {
DeboaRequest::get(self)?.build()
}
}
impl IntoRequest for String {
fn into_request(self) -> Result<DeboaRequest> {
DeboaRequest::get(self)?.build()
}
}
impl IntoRequest for Url {
fn into_request(self) -> Result<DeboaRequest> {
DeboaRequest::get(self)?.build()
}
}
#[deprecated(note = "Use FetchWith trait instead", since = "0.0.8")]
#[async_trait]
pub trait Fetch {
async fn fetch<T>(&self, client: T) -> Result<DeboaResponse>
where
T: AsMut<Deboa> + Send;
}
#[async_trait]
#[allow(deprecated)]
impl Fetch for &str {
async fn fetch<T>(&self, client: T) -> Result<DeboaResponse>
where
T: AsMut<Deboa> + Send,
{
DeboaRequest::get(*self)?
.send_with(client)
.await
}
}
#[async_trait]
pub trait FetchWith {
async fn fetch_with<T>(&self, client: T) -> Result<DeboaResponse>
where
T: AsMut<Deboa> + Send;
}
#[async_trait]
impl FetchWith for &str {
async fn fetch_with<T>(&self, client: T) -> Result<DeboaResponse>
where
T: AsMut<Deboa> + Send,
{
DeboaRequest::get(*self)?
.send_with(client)
.await
}
}
#[async_trait]
impl FetchWith for String {
async fn fetch_with<T>(&self, client: T) -> Result<DeboaResponse>
where
T: AsMut<Deboa> + 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: Arc<[u8]>,
form: Option<Form>,
}
impl DeboaRequestBuilder {
pub fn retries(mut self, retries: u32) -> Self {
self.retries = retries;
self
}
pub fn method(mut self, method: http::Method) -> Self {
self.method = method;
self
}
pub fn raw_body(mut self, body: &[u8]) -> Self {
self.body = body.into();
self
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
pub fn header(mut self, key: HeaderName, value: &str) -> Self {
self.headers
.insert(key, HeaderValue::from_str(value).unwrap());
self
}
pub fn cookies(mut self, cookies: HashMap<String, DeboaCookie>) -> Self {
self.cookies = Some(cookies);
self
}
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
}
pub fn form(mut self, form: Form) -> Self {
self.form = Some(form);
self
}
pub fn text(mut self, text: &str) -> Self {
self.body = text
.as_bytes()
.into();
self
}
pub fn body_as<T: RequestBody, B: Serialize>(mut self, body_type: T, body: B) -> Result<Self> {
self.body = body_type
.serialize(body)?
.into();
Ok(self)
}
#[inline]
pub fn bearer_auth(self, token: &str) -> Self {
self.header(header::AUTHORIZATION, format!("Bearer {token}").as_str())
}
pub fn basic_auth(self, username: &str, password: &str) -> Self {
self.header(
header::AUTHORIZATION,
format!("Basic {}", STANDARD.encode(format!("{username}:{password}"))).as_str(),
)
}
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(form) = self.form {
request.set_form(form);
}
Ok(request)
}
#[deprecated(note = "Use `send_with` method instead", since = "0.0.8")]
pub async fn go<T>(self, mut client: T) -> Result<DeboaResponse>
where
T: AsMut<Deboa>,
{
client
.as_mut()
.execute(self.build()?)
.await
}
pub async fn send_with<T>(self, mut client: T) -> Result<DeboaResponse>
where
T: AsMut<Deboa>,
{
client
.as_mut()
.execute(self.build()?)
.await
}
}
pub struct DeboaRequest {
url: Arc<Url>,
headers: HeaderMap,
cookies: Option<HashMap<String, DeboaCookie>>,
retries: u32,
method: http::Method,
body: Arc<[u8]>,
}
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)
.field("body", &self.body)
.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 {
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() {
return Err(DeboaError::Request(RequestError::Parse {
message: "Missing method in request format".into(),
}));
}
let url_cap = captures.get(2);
if url_cap.is_none() {
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(|_| {
DeboaError::Request(RequestError::Parse {
message: "Invalid header name".into(),
})
})?;
let header_value = HeaderValue::from_bytes(
header
.1
.trim()
.as_bytes(),
)
.map_err(|_| {
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: body.into(),
})
}
}
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 {
pub fn at<T: IntoUrl>(url: T, method: http::Method) -> Result<DeboaRequestBuilder> {
let parsed_url = url.into_url();
if let Err(e) = parsed_url {
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: Arc::new([]),
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))
}
pub fn set_method(&mut self, method: http::Method) -> &mut Self {
self.method = method;
self
}
#[inline]
pub fn method(&self) -> &http::Method {
&self.method
}
pub fn set_url<T: IntoUrl>(&mut self, url: T) -> Result<&mut Self> {
let parsed_url = url.into_url();
if let Err(e) = parsed_url {
return Err(DeboaError::Request(RequestError::UrlParse { message: e.to_string() }));
}
let parsed_url = parsed_url.unwrap();
if self.has_header(&header::HOST) {
self.headers
.remove(&header::HOST);
self.add_header(HOST, parsed_url.authority());
}
self.url = parsed_url.into();
Ok(self)
}
#[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
}
pub fn add_header(&mut self, key: HeaderName, value: &str) -> &mut Self {
self.headers
.insert(key, HeaderValue::from_str(value).unwrap());
self
}
#[inline]
fn has_header(&self, key: &HeaderName) -> bool {
self.headers
.contains_key(key)
}
pub fn add_bearer_auth(&mut self, token: &str) -> &mut Self {
let auth = format!("Bearer {token}");
self.add_header(header::AUTHORIZATION, &auth);
self
}
pub fn add_basic_auth(&mut self, username: &str, password: &str) -> &mut Self {
let auth = format!("Basic {}", STANDARD.encode(format!("{username}:{password}")));
self.add_header(header::AUTHORIZATION, &auth);
self
}
pub fn add_cookie(&mut self, cookie: DeboaCookie) -> &mut 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
}
pub fn remove_cookie(&mut self, name: &str) -> &mut Self {
if let Some(cookies) = &mut self.cookies {
cookies.remove(name);
}
self
}
pub fn has_cookie(&self, name: &str) -> bool {
if let Some(cookies) = &self.cookies {
cookies.contains_key(name)
} else {
false
}
}
pub fn set_cookies(&mut self, cookies: HashMap<String, DeboaCookie>) -> &mut Self {
self.cookies = Some(cookies);
self
}
pub fn cookies(&self) -> Option<&HashMap<String, DeboaCookie>> {
self.cookies
.as_ref()
}
pub fn set_form(&mut self, form: Form) -> &mut Self {
let (content_type, body) = match form {
Form::EncodedForm(form) => (form.content_type(), form.build()),
Form::MultiPartForm(form) => (form.content_type(), form.build()),
};
self.add_header(header::CONTENT_TYPE, &content_type);
self.set_raw_body(&body);
self
}
pub fn set_text(&mut self, text: String) -> &mut Self {
self.set_raw_body(text.as_bytes());
self
}
pub fn set_raw_body(&mut self, body: &[u8]) -> &mut Self {
self.add_header(
header::CONTENT_LENGTH,
&body
.len()
.to_string(),
);
self.body = body.into();
self
}
#[inline]
pub fn raw_body(&self) -> &[u8] {
&self.body
}
pub fn set_body_as<T: RequestBody, B: Serialize>(
&mut self,
body_type: T,
body: B,
) -> Result<&mut Self> {
body_type.register_content_type(self);
let body = body_type.serialize(body)?;
self.set_raw_body(&body);
Ok(self)
}
}
mod private {
pub trait Sealed {}
}
impl private::Sealed for DeboaRequest {}
impl private::Sealed for &str {}
impl private::Sealed for String {}
impl private::Sealed for Url {}