use std::sync::mpsc::Sender;
use rand::Rng;
pub struct DispatchedRequest {
pub respone_sender: Sender<DispatchedResponse>,
pub request: Request,
}
pub enum DispatchedResponse {
Success(Response),
Error(anyhow::Error),
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct Request {
pub url: String,
pub method: HttpMethod,
pub headers: Vec<(String, String)>,
pub body: String,
#[serde(default)]
pub follow_redirects: bool,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub status_code: u16,
pub headers: Vec<(String, String)>,
pub body: ResponseBody,
}
impl Response {
pub fn content_length(&self) -> Option<usize> {
self.headers
.iter()
.find(|(k, _)| k.to_lowercase() == "content-length")
.and_then(|(_, v)| v.parse().ok())
.or(match &self.body {
ResponseBody::Text(text) => Some(text.len()),
ResponseBody::Json { value: _, length } => Some(*length),
ResponseBody::Image(_path) | ResponseBody::Binary(_path) => None,
ResponseBody::Empty => None,
})
}
pub fn is_redirect(&self) -> bool {
self.status_code >= 300 && self.status_code < 400
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub enum ResponseBody {
Text(String),
Json {
value: serde_json::Value,
length: usize,
},
Image(std::path::PathBuf),
Binary(std::path::PathBuf),
Empty,
}
impl Drop for ResponseBody {
fn drop(&mut self) {
match self {
ResponseBody::Image(path) | ResponseBody::Binary(path) => {
let _ = std::fs::remove_file(path);
}
_ => (),
}
}
}
#[derive(Debug, PartialEq, Clone, Copy, serde::Deserialize, serde::Serialize)]
pub enum HttpMethod {
Get,
Post,
Put,
}
impl HttpMethod {
pub fn as_str(&self) -> &str {
match self {
HttpMethod::Get => "GET",
HttpMethod::Post => "POST",
HttpMethod::Put => "PUT",
}
}
pub fn can_have_body(&self) -> bool {
match self {
HttpMethod::Get => false,
HttpMethod::Post | HttpMethod::Put => true,
}
}
pub fn iter() -> impl Iterator<Item = HttpMethod> {
[HttpMethod::Get, HttpMethod::Post, HttpMethod::Put]
.iter()
.copied()
}
}
impl Request {
pub fn to_curl(&self) -> String {
let mut curl = format!("curl -X {} '{}'", self.method.as_str(), self.url);
for (key, value) in &self.headers {
if !key.is_empty() && !value.is_empty() {
curl.push_str(&format!(" -H '{}: {}'", key, value));
}
}
if self.method.can_have_body() && !self.body.is_empty() {
curl.push_str(&format!(" -d '{}'", self.body.replace('\'', "'\\''")));
}
if self.follow_redirects {
curl.push_str(" -L");
}
curl
}
pub fn to_http_format(&self) -> String {
let (path, host) = if let Ok(parsed_url) = self.url.parse::<url::Url>() {
let path = if parsed_url.path().is_empty() { "/" } else { parsed_url.path() };
let host = parsed_url.host_str().unwrap_or("localhost");
(path.to_string(), host.to_string())
} else {
("/".to_string(), "localhost".to_string())
};
let mut http = format!("{} {} HTTP/1.1\r\n", self.method.as_str(), path);
http.push_str(&format!("Host: {}\r\n", host));
for (key, value) in &self.headers {
if !key.is_empty() && !value.is_empty() {
http.push_str(&format!("{}: {}\r\n", key, value));
}
}
http.push_str("\r\n");
if self.method.can_have_body() && !self.body.is_empty() {
http.push_str(&self.body);
}
http
}
}
impl Default for Request {
fn default() -> Self {
const URLS: [&str; 6] = [
"https://echo.free.beeceptor.com",
"https://picsum.photos/800",
"https://ifconfig.co/json",
"https://api.coingecko.com/api/v3/coins/bitcoin",
"https://randomuser.me/api/",
"https://v2.jokeapi.dev/joke/Any",
];
let mut rng = rand::thread_rng();
let rnd_idx = rng.gen_range(0..URLS.len());
let url = URLS[rnd_idx];
Self {
url: url.to_string(),
method: HttpMethod::Get,
headers: vec![
("User-Agent".to_string(), "httpman".to_string()),
("Accept".to_string(), "application/json".to_string()),
("Content-Type".to_string(), "application/json".to_string()),
],
body: "".to_string(),
follow_redirects: false,
}
}
}