hur 0.1.0

CLI HTTP requests, nothing more complicated than that.
Documentation
use std::io::Read;

use super::http::{headers::Headers, Method};
use super::logs;

use crate::error;
use crate::error::Error;

use clap::{crate_authors, crate_name, crate_version, Arg, ArgMatches};

pub mod output;
use output::Output;

pub struct Input {
    pub url: String,
    pub method: Method,
    pub headers: Headers,
    pub body: Option<String>,
    pub allow_proxy: bool,
    pub timeout: u64,
}

pub fn parse_args(args: Vec<String>) -> Result<(Input, Output), Error> {
    let matches = use_clap(&args);
    let input = parse_input(&matches)?;
    let output = parse_output(&matches);
    if matches.is_present("info") {
        enable_logging()?;
    }

    Ok((input, output))
}

fn parse_input(matches: &ArgMatches) -> Result<Input, Error> {
    let mut headers = headers(matches)?;
    let body = collect_body(
        matches.value_of("body"),
        matches.value_of("json"),
        &mut headers,
    )?;

    let timeout = match matches.value_of("timeout") {
        Some(timeout) => timeout.parse::<u64>().unwrap(), // TODO Solve better
        None => 10,
    };

    Ok(Input {
        url: matches.value_of("url").unwrap().to_string(),
        method: get_method(matches.value_of("method").unwrap()),
        headers,
        body,
        allow_proxy: !(matches.is_present("no-proxy")),
        timeout,
    })
}

fn parse_output(matches: &ArgMatches) -> Output {
    Output {
        verbose: matches.is_present("verbose"),
        no_body: matches.is_present("no-body"),
    }
}

fn headers(matches: &ArgMatches) -> Result<Headers, Error> {
    let mut headers = single_headers(matches.values_of("header"));
    if let Some(h) = json_headers(matches.value_of("headers"))? {
        headers.append(h)
    };
    Ok(headers)
}

fn single_headers(headers_option: Option<clap::Values>) -> Headers {
    match headers_option {
        Some(values) => {
            let mut headers = Headers::new();
            for val in values {
                let splits: Vec<&str> = val.splitn(2, ':').collect();
                headers.add(splits[0], splits[1]);
            }
            headers
        }
        None => Headers::new(),
    }
}

fn json_headers(headers_json: Option<&str>) -> Result<Option<Headers>, Error> {
    match headers_json {
        Some(value) => {
            let json_string = match value.ends_with(".json") {
                true => read_file(value)?,
                false => value.to_string(),
            };
            let map: std::collections::HashMap<String, String> =
                serde_json::from_str(&json_string)?;
            Ok(Some(Headers::from(map)))
        }
        None => Ok(None),
    }
}

fn read_file(path: &str) -> Result<String, Error> {
    let mut file = std::fs::File::open(path)?;
    let mut buf = String::new();
    file.read_to_string(&mut buf)?;
    Ok(buf)
}

fn collect_body(
    body_option: Option<&str>,
    json_option: Option<&str>,
    headers: &mut Headers,
) -> Result<Option<String>, Error> {
    let mut body: Option<String> = None;
    if let Some(body_str) = body_option {
        body = Some(body_str.to_string());
    }
    if let Some(body_str) = json_option {
        match serde_json::from_str::<serde_json::Value>(body_str) {
            Ok(_) => {
                body = Some(body_str.to_string());
                headers.add("Content-Type", "application/json");
            }
            Err(why) => error!(&why.to_string()),
        };
    }

    Ok(body)
}

fn enable_logging() -> Result<(), log::SetLoggerError> {
    static LOGGER: logs::Logger = logs::Logger;
    log::set_logger(&LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info))
}

fn get_method(method: &str) -> Method {
    match method.to_lowercase().as_str() {
        "get" => Method::Get,
        "post" => Method::Post,
        "put" => Method::Put,
        "delete" => Method::Delete,
        "patch" => Method::Patch,
        "connect" => Method::Connect,
        "options" => Method::Options,
        "trace" => Method::Trace,
        "head" => Method::Head,
        _ => Method::Get,
    }
}

fn use_clap(args: &[String]) -> ArgMatches {
    return clap::app_from_crate!(crate_name!())
        .version(crate_version!())
        .author(crate_authors!("\n"))
        .arg(
            Arg::new("url")
                .help("The URL for the request")
                .required(true)
                .takes_value(true),
        )
        .arg(
            Arg::new("verbose")
                .long("verbose")
                .short('v')
                .help("Full request and response output in JSON"),
        )
        .arg(Arg::new("info").long("info").help("Show info logging"))
        .arg(
            Arg::new("method")
                .takes_value(true)
                .short('m')
                .long("method")
                .default_value("get")
                .possible_values(&[
                    "get", "GET", "post", "POST", "put", "PUT", "trace", "TRACE", "patch", "PATCH",
                    "delete", "DELETE", "head", "HEAD", "options", "OPTIONS", "connect", "CONNECT",
                ]),
        )
        .arg(
            Arg::new("header")
                .help("Header for request")
                .takes_value(true)
                .short('h')
                .long("header")
                .multiple_occurrences(true),
        )
        .arg(
            Arg::new("headers")
                .help("Headers as a JSON string or JSON file")
                .takes_value(true)
                .long("headers"),
        )
        .arg(
            Arg::new("body")
                .help("Body for request")
                .long("body")
                .conflicts_with("json")
                .takes_value(true),
        )
        .arg(
            Arg::new("json")
                .help("Send body with Content-Type:application/json")
                .long("json")
                .conflicts_with("body")
                .takes_value(true),
        )
        .arg(
            Arg::new("no-body")
                .help("Don't print response body")
                .long("no-body"),
        )
        .arg(
            Arg::new("no-proxy")
                .help("Do not proxy request")
                .long("no-proxy"),
        )
        .arg(
            Arg::new("timeout")
                .help("The read timeout in seconds for the request")
                .long("timeout")
                .takes_value(true),
        )
        .get_matches_from(args);
}

#[test]
fn test_collect_body() {
    let mut headers = Headers::new();
    let body_opt = Some("form1:value2");
    let json_opt = None;

    let body = collect_body(body_opt, json_opt, &mut headers).unwrap();
    assert!(body.is_some());
    assert_eq!(body.unwrap(), "form1:value2");
}

#[test]
fn test_collect_json_body() {
    let mut headers = Headers::new();
    let body_opt = None;
    let json_opt = Some(r#"{"key":"value"}"#);

    let body = collect_body(body_opt, json_opt, &mut headers).unwrap();
    assert!(body.is_some());
    assert_eq!(body.unwrap(), r#"{"key":"value"}"#);
    assert!(headers.get("Content-Type").is_some());
}