refactor + docs

This commit is contained in:
MeexReay 2025-01-19 11:25:38 +03:00
parent bda82c9c7c
commit 38d4f30f55
14 changed files with 184 additions and 129 deletions

View File

@ -4,9 +4,14 @@ use async_trait::async_trait;
use serde_json::Value;
use tokio::{fs, io::{AsyncReadExt, AsyncWrite, AsyncWriteExt}};
use crate::ezhttp::{split_bytes, split_bytes_once};
use super::{error::HttpError, headers::Headers, read_line_crlf, Sendable};
use super::{
split_bytes,
split_bytes_once,
error::HttpError,
headers::Headers,
read_line_crlf,
Sendable
};
#[derive(Debug, Clone)]
pub struct Body {

View File

@ -2,12 +2,14 @@ use crate::{error::HttpError, headers::Headers, prelude::HttpResponse, request::
use super::{send_request, Proxy};
/// Client that sends http requests
pub struct HttpClient {
proxy: Proxy,
verify: bool,
headers: Headers
}
/// [`HttpClient`](HttpClient) builder
pub struct ClientBuilder {
proxy: Proxy,
verify: bool,
@ -15,6 +17,7 @@ pub struct ClientBuilder {
}
impl ClientBuilder {
/// Create a client builder
pub fn new() -> ClientBuilder {
ClientBuilder {
proxy: Proxy::None,
@ -23,6 +26,7 @@ impl ClientBuilder {
}
}
/// Build a client
pub fn build(self) -> HttpClient {
HttpClient {
proxy: self.proxy,
@ -31,22 +35,25 @@ impl ClientBuilder {
}
}
/// Set client proxy
pub fn proxy(mut self, proxy: Proxy) -> Self {
self.proxy = proxy;
self
}
/// Set is client have to verify ssl certificate
pub fn verify(mut self, verify: bool) -> Self {
self.verify = verify;
self
}
/// Set default headers
pub fn headers(mut self, headers: Headers) -> Self {
self.headers = headers;
self
}
/// Set default header
pub fn header(mut self, name: impl ToString, value: impl ToString) -> Self {
self.headers.put(name, value.to_string());
self
@ -54,16 +61,19 @@ impl ClientBuilder {
}
impl HttpClient {
/// Get new HttpClient builder
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
/// Sends a request and receives a response
pub async fn send(&self, request: HttpRequest) -> Result<HttpResponse, HttpError> {
send_request(request, self.verify, self.proxy.clone(), self.headers.clone()).await
}
}
impl Default for HttpClient {
/// Create default HttpClient
fn default() -> Self {
ClientBuilder::new().build()
}

View File

@ -18,8 +18,6 @@ pub use req_builder::*;
pub use client::*;
pub use proxy::*;
// TODO: proxy support
async fn send_request(request: HttpRequest, ssl_verify: bool, proxy: Proxy, headers: Headers) -> Result<HttpResponse, HttpError> {
let mut request = request.clone();

View File

@ -4,6 +4,7 @@ use serde_json::Value;
use super::{super::body::{Body, Part}, gen_multipart_boundary, super::headers::Headers, super::request::{HttpRequest, URL}};
/// Builder for [`HttpRequest`](HttpRequest)
pub struct RequestBuilder {
method: String,
url: URL,
@ -12,6 +13,7 @@ pub struct RequestBuilder {
}
impl RequestBuilder {
/// Create builder with a custom method
pub fn new(method: String, url: URL) -> Self {
RequestBuilder {
method,
@ -21,56 +23,82 @@ impl RequestBuilder {
}
}
/// Create builder for a GET request
pub fn get(url: URL) -> Self { Self::new("GET".to_string(), url) }
/// Create builder for a HEAD request
pub fn head(url: URL) -> Self { Self::new("HEAD".to_string(), url) }
/// Create builder for a POST request
pub fn post(url: URL) -> Self { Self::new("POST".to_string(), url) }
/// Create builder for a PUT request
pub fn put(url: URL) -> Self { Self::new("PUT".to_string(), url) }
/// Create builder for a DELETE request
pub fn delete(url: URL) -> Self { Self::new("DELETE".to_string(), url) }
/// Create builder for a CONNECT request
pub fn connect(url: URL) -> Self { Self::new("CONNECT".to_string(), url) }
/// Create builder for a OPTIONS request
pub fn options(url: URL) -> Self { Self::new("OPTIONS".to_string(), url) }
/// Create builder for a TRACE request
pub fn trace(url: URL) -> Self { Self::new("TRACE".to_string(), url) }
/// Create builder for a PATCH request
pub fn patch(url: URL) -> Self { Self::new("PATCH".to_string(), url) }
/// Set request url
pub fn url(mut self, url: URL) -> Self {
self.url = url;
self
}
/// Set request method
pub fn method(mut self, method: String) -> Self {
self.method = method;
self
}
/// Set headers
pub fn headers(mut self, headers: Headers) -> Self {
self.headers = headers;
self
}
/// Set header
pub fn header(mut self, name: impl ToString, value: impl ToString) -> Self {
self.headers.put(name, value.to_string());
self
}
/// Set body
pub fn body(mut self, body: Body) -> Self {
self.body = Some(body);
self
}
/// Set text as body
pub fn text(mut self, text: impl ToString) -> Self {
self.body = Some(Body::from_text(text.to_string().as_str()));
self
}
/// Set json as body
pub fn json(mut self, json: Value) -> Self {
self.body = Some(Body::from_json(json));
self
}
/// Set raw bytes as body
pub fn bytes(mut self, bytes: &[u8]) -> Self {
self.body = Some(Body::from_bytes(bytes));
self
}
/// Set multipart as body
pub fn multipart(mut self, parts: &[Part]) -> Self {
let boundary = gen_multipart_boundary();
self.headers.put("Content-Type", format!("multipart/form-data; boundary={}", boundary.clone()));
@ -78,16 +106,19 @@ impl RequestBuilder {
self
}
/// Set query in url
pub fn url_query(mut self, query: &[(impl ToString, impl ToString)]) -> Self {
self.url.query = HashMap::from_iter(query.iter().map(|o| (o.0.to_string(), o.1.to_string())));
self
}
/// Set query as body
pub fn body_query(mut self, query: &[(impl ToString, impl ToString)]) -> Self {
self.body = Some(Body::from_query(HashMap::from_iter(query.iter().map(|o| (o.0.to_string(), o.1.to_string())))));
self
}
/// Build request
pub fn build(self) -> HttpRequest {
HttpRequest {
url: self.url,

View File

@ -1,6 +1,6 @@
use std::error::Error;
/// Http error
/// Http library errors
#[derive(Debug)]
pub enum HttpError {
ReadLineEof,

View File

@ -1,120 +0,0 @@
pub mod error;
pub mod headers;
pub mod request;
pub mod response;
pub mod body;
pub mod server;
pub mod client;
pub mod prelude {
pub use super::error::*;
pub use super::headers::*;
pub use super::request::*;
pub use super::response::*;
pub use super::response::status_code::*;
pub use super::body::*;
pub use super::server::*;
pub use super::server::handler::*;
pub use super::server::starter::*;
// pub use super::client::*;
pub use super::*;
}
use error::HttpError;
use rand::Rng;
use tokio::{io::{AsyncReadExt, AsyncWrite}, net::TcpStream};
use tokio_io_timeout::TimeoutStream;
use async_trait::async_trait;
const CHARS: &str = "qwertyuiopasdfghjklzxcvbnm0123456789QWERTYUIOPASDFGHJKLZXCVBNM'()+_,-./:=?";
pub fn gen_multipart_boundary() -> String {
let range = 20..40;
let length: usize = rand::thread_rng().gen_range(range);
[0..length].iter().map(|_|
String::from(CHARS.chars()
.collect::<Vec<char>>()
.get(rand::thread_rng()
.gen_range(0..CHARS.len())
).unwrap().clone()
)
).collect::<Vec<String>>().join("")
}
fn split_bytes_once(bytes: &[u8], sep: &[u8]) -> (Vec<u8>, Vec<u8>) {
if let Some(index) = bytes.windows(sep.len())
.enumerate()
.filter(|o| o.1 == sep)
.map(|o| o.0)
.next() {
let t = bytes.split_at(index);
(t.0.to_vec(), t.1.split_at(sep.len()).1.to_vec())
} else {
(Vec::from(bytes), Vec::new())
}
}
fn split_bytes(bytes: &[u8], sep: &[u8]) -> Vec<Vec<u8>> {
if bytes.len() >= sep.len() {
let indexes: Vec<usize> = bytes.windows(sep.len())
.enumerate()
.filter(|o| o.1 == sep)
.map(|o| o.0)
.collect();
let mut parts: Vec<Vec<u8>> = Vec::new();
let mut now_part: Vec<u8> = Vec::new();
let mut i = 0usize;
loop {
if i >= bytes.len() {
break;
}
if indexes.contains(&i) {
parts.push(now_part.clone());
now_part.clear();
i += sep.len();
continue;
}
now_part.push(bytes[i]);
i += 1;
}
parts.push(now_part);
parts
} else {
vec![Vec::from(bytes)]
}
}
async fn read_line(data: &mut (impl AsyncReadExt + Unpin)) -> Result<String, HttpError> {
let mut line = Vec::new();
loop {
let mut buffer = vec![0;1];
data.read_exact(&mut buffer).await.or(Err(HttpError::ReadLineEof))?;
let char = buffer[0];
line.push(char);
if char == 0x0a {
break;
}
}
String::from_utf8(line).or(Err(HttpError::ReadLineUnknown))
}
async fn read_line_crlf(data: &mut (impl AsyncReadExt + Unpin)) -> Result<String, HttpError> {
match read_line(data).await {
Ok(i) => Ok(i[..i.len() - 2].to_string()),
Err(e) => Err(e),
}
}
#[async_trait]
pub trait Sendable: Send + Sync {
async fn send(
&self,
stream: &mut (dyn AsyncWrite + Unpin + Send + Sync),
) -> Result<(), HttpError>;
fn as_box(self) -> Box<dyn Sendable>;
}
pub type Stream = TimeoutStream<TcpStream>;

View File

@ -1,3 +1,120 @@
pub mod ezhttp;
pub mod error;
pub mod headers;
pub mod request;
pub mod response;
pub mod body;
pub mod server;
pub mod client;
pub use ezhttp::*;
pub mod prelude {
pub use super::error::*;
pub use super::headers::*;
pub use super::request::*;
pub use super::response::*;
pub use super::response::status_code::*;
pub use super::body::*;
pub use super::server::*;
pub use super::server::handler::*;
pub use super::server::starter::*;
pub use super::client::*;
pub use super::*;
}
use error::HttpError;
use rand::Rng;
use tokio::{io::{AsyncReadExt, AsyncWrite}, net::TcpStream};
use tokio_io_timeout::TimeoutStream;
use async_trait::async_trait;
const CHARS: &str = "qwertyuiopasdfghjklzxcvbnm0123456789QWERTYUIOPASDFGHJKLZXCVBNM'()+_,-./:=?";
pub fn gen_multipart_boundary() -> String {
let range = 20..40;
let length: usize = rand::thread_rng().gen_range(range);
[0..length].iter().map(|_|
String::from(CHARS.chars()
.collect::<Vec<char>>()
.get(rand::thread_rng()
.gen_range(0..CHARS.len())
).unwrap().clone()
)
).collect::<Vec<String>>().join("")
}
fn split_bytes_once(bytes: &[u8], sep: &[u8]) -> (Vec<u8>, Vec<u8>) {
if let Some(index) = bytes.windows(sep.len())
.enumerate()
.filter(|o| o.1 == sep)
.map(|o| o.0)
.next() {
let t = bytes.split_at(index);
(t.0.to_vec(), t.1.split_at(sep.len()).1.to_vec())
} else {
(Vec::from(bytes), Vec::new())
}
}
fn split_bytes(bytes: &[u8], sep: &[u8]) -> Vec<Vec<u8>> {
if bytes.len() >= sep.len() {
let indexes: Vec<usize> = bytes.windows(sep.len())
.enumerate()
.filter(|o| o.1 == sep)
.map(|o| o.0)
.collect();
let mut parts: Vec<Vec<u8>> = Vec::new();
let mut now_part: Vec<u8> = Vec::new();
let mut i = 0usize;
loop {
if i >= bytes.len() {
break;
}
if indexes.contains(&i) {
parts.push(now_part.clone());
now_part.clear();
i += sep.len();
continue;
}
now_part.push(bytes[i]);
i += 1;
}
parts.push(now_part);
parts
} else {
vec![Vec::from(bytes)]
}
}
async fn read_line(data: &mut (impl AsyncReadExt + Unpin)) -> Result<String, HttpError> {
let mut line = Vec::new();
loop {
let mut buffer = vec![0;1];
data.read_exact(&mut buffer).await.or(Err(HttpError::ReadLineEof))?;
let char = buffer[0];
line.push(char);
if char == 0x0a {
break;
}
}
String::from_utf8(line).or(Err(HttpError::ReadLineUnknown))
}
async fn read_line_crlf(data: &mut (impl AsyncReadExt + Unpin)) -> Result<String, HttpError> {
match read_line(data).await {
Ok(i) => Ok(i[..i.len() - 2].to_string()),
Err(e) => Err(e),
}
}
#[async_trait]
pub trait Sendable: Send + Sync {
async fn send(
&self,
stream: &mut (dyn AsyncWrite + Unpin + Send + Sync),
) -> Result<(), HttpError>;
fn as_box(self) -> Box<dyn Sendable>;
}
pub type Stream = TimeoutStream<TcpStream>;

View File

@ -6,6 +6,7 @@ use std::{
use async_trait::async_trait;
use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
/// Request URL
#[derive(Clone, Debug)]
pub struct URL {
pub path: String,
@ -35,6 +36,8 @@ impl URL {
}
}
/// Turns URL object to url string without scheme, domain, port
/// Example: /123.html?k=v#anc
pub fn to_path_string(&self) -> String {
format!("{}{}{}", self.path, if self.query.is_empty() {
String::new()
@ -49,6 +52,8 @@ impl URL {
})
}
/// Turns string without scheme, domain, port to URL object
/// Example of string: /123.html?k=v#anc
pub fn from_path_string(s: &str, scheme: String, domain: String, port: u16) -> Option<Self> {
let (s, anchor) = s.split_once("#").unwrap_or((s, ""));
let (path, query) = s.split_once("?").unwrap_or((s, ""));
@ -69,6 +74,9 @@ impl URL {
impl FromStr for URL {
type Err = HttpError;
/// Turns url string to URL object
/// Example: https://domain.com:999/123.html?k=v#anc
/// Example 2: http://exampl.eu/sing
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (scheme, s) = s.split_once("://").ok_or(HttpError::UrlError)?;
let (host, s) = s.split_once("/").unwrap_or((s, ""));
@ -96,6 +104,9 @@ impl FromStr for URL {
}
impl ToString for URL {
/// Turns URL object to string
/// Example: https://domain.com:999/123.html?k=v#anc
/// Example 2: http://exampl.eu/sing
fn to_string(&self) -> String {
format!("{}://{}{}", self.scheme, {
if (self.scheme == "http" && self.port != 80) || (self.scheme == "https" && self.port != 443) {
@ -173,6 +184,7 @@ impl HttpRequest {
))
}
/// Get multipart parts (requires Content-Type header)
pub fn get_multipart(&self) -> Option<Vec<Part>> {
let boundary = self.headers.get("content-type")?
.split(";")
@ -182,6 +194,7 @@ impl HttpRequest {
Some(self.body.as_multipart(boundary))
}
/// Set multipart parts (modifies Content-Type header)
pub fn set_multipart(&mut self, parts: Vec<Part>) -> Option<()> {
let boundary = gen_multipart_boundary();
self.headers.put("Content-Type", format!("multipart/form-data; boundary={}", boundary.clone()));
@ -189,6 +202,7 @@ impl HttpRequest {
Some(())
}
/// Create new request builder
pub fn builder(method: String, url: URL) -> RequestBuilder {
RequestBuilder::new(method, url)
}