hur 0.1.0

CLI HTTP requests, nothing more complicated than that.
Documentation
use std::convert::TryFrom;
use std::net::{SocketAddr, ToSocketAddrs};

use super::headers::Headers;
use super::{Method, Scheme};
use serde::Serialize;
use url::Url;

use crate::error::Error;
use crate::proxy::should_proxy;

#[derive(Serialize)]
pub struct Request {
    #[serde(skip)]
    pub url: Url,
    #[serde(skip)]
    pub proxy: bool,
    #[serde(skip)]
    pub servers: Vec<SocketAddr>,
    #[serde(rename = "url")]
    pub full_path: String,
    pub scheme: Scheme,
    pub protocol: String,
    pub method: Method,
    path: String,
    pub headers: Headers,
    #[serde(skip_serializing_if = "Option::is_none")]
    body: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    query: Option<String>,
}

impl Request {
    pub fn new(url: Url, method: Method, headers: Headers) -> Result<Request, Error> {
        let scheme = Scheme::try_from(url.scheme())?;
        let url_servers = find_socket_addresses(&url, &scheme)?;
        let (servers, proxy) = match should_proxy(&url, &url_servers, &scheme)? {
            Some(servers) => (servers, true),
            None => (url_servers, false),
        };

        Ok(Request {
            proxy,
            servers,
            full_path: url.to_string(),
            scheme,
            protocol: String::from("HTTP/1.1"),
            method,
            path: String::from(url.path()),
            headers: standard_headers(headers, &url.host().unwrap().to_string()),
            body: None,
            query: url.query().map_or_else(|| None, |s| Some(String::from(s))),
            url,
        })
    }

    pub fn with_body(
        url: Url,
        method: Method,
        headers: Headers,
        body: &str,
    ) -> Result<Request, Error> {
        let mut request = Request::new(url, method, headers)?;
        request.body = Some(body.to_string());
        request
            .headers
            .add("Content-Length", &body.as_bytes().len().to_string());
        Ok(request)
    }

    fn build_request(&self, path: &str) -> String {
        let mut message = self.make_status_line(path);
        self.add_headers(&mut message);
        self.add_body(&mut message);
        message.push_str("\r\n\r\n");
        message
    }

    pub fn build(&self) -> String {
        let path = match (self.proxy, &self.scheme) {
            (true, Scheme::Http) => &self.full_path,
            _ => &self.path,
        };
        self.build_request(path)
    }

    fn make_status_line(&self, path: &str) -> String {
        let path = match &self.query {
            Some(query) => format!("{path}?{query}"),
            None => path.to_string(),
        };
        format!(
            "{method} {path} {protocol}\r\n",
            method = self.method.to_string().to_uppercase(),
            path = path,
            protocol = self.protocol,
        )
    }

    fn add_body(&self, message: &mut String) {
        if let Some(body) = &self.body {
            message.push_str("\r\n");
            message.push_str(body);
        }
    }

    fn add_headers(&self, message: &mut String) {
        for (key, value_vec) in self.headers.iter() {
            for val in value_vec {
                message.push_str(&format!(
                    "{key}: {value}\r\n",
                    key = key,
                    value = val.trim(),
                ));
            }
        }
    }
}

fn find_socket_addresses(url: &Url, scheme: &Scheme) -> Result<Vec<SocketAddr>, Error> {
    let mut server_details = String::new();
    match url.domain() {
        Some(domain) => server_details.push_str(domain),
        None => server_details.push_str(&url.host().unwrap().to_string()),
    };
    server_details.push(':');
    match url.port() {
        Some(port) => server_details.push_str(&port.to_string()),
        None => match scheme {
            Scheme::Https => server_details.push_str("443"),
            Scheme::Http => server_details.push_str("80"),
        },
    }
    Ok(server_details.to_socket_addrs()?.collect())
}

fn standard_headers(input_headers: Headers, host: &str) -> Headers {
    let mut hs = Headers::new();
    hs.add(
        "User-Agent",
        &format!("{}/{}", clap::crate_name!(), clap::crate_version!()),
    );
    hs.add("Host", host);
    hs.add("Connection", "close");
    hs.append(input_headers);
    hs
}