httpman 0.1.5

A fast, modern HTTP client with tab management and request persistence
Documentation
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);
        
        // Add Host header from URL
        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,
        }
    }
}