boomack-cli 0.5.0

CLI client for Boomack
use std::io::Write;
use sysinfo::{System, ProcessesToUpdate, ProcessRefreshKind, get_current_pid};
use termcolor::{Buffer, BufferWriter, WriteColor, ColorSpec, Color};
use http_types::Url;
use boomack::client::json::pprint_json;
use boomack::client::api::{
    ClientRequest,
    ClientRequestMethod,
    ClientRequestBody,
    build_request_url,
};
use super::{CliConfig};

fn request_method_name(client_request: &ClientRequest) -> &str {
    match client_request.method {
        ClientRequestMethod::GET => "GET",
        ClientRequestMethod::POST => "POST",
        ClientRequestMethod::PUT => "PUT",
        ClientRequestMethod::DELETE => "DELETE",
    }
}

type IoResult = Result<(), std::io::Error>;

fn reset_color(buf: &mut Buffer) -> IoResult {
    buf.set_color(ColorSpec::new().set_reset(true))?;
    IoResult::Ok(())
}

fn set_color(buf: &mut Buffer, color: Color, intense: bool, dimmed: bool) -> IoResult {
    buf.set_color(ColorSpec::new()
        .set_fg(Some(color))
        .set_intense(intense)
        .set_dimmed(dimmed))?;
    IoResult::Ok(())
}

fn print_request_plain(cfg: &CliConfig, client_request: ClientRequest) -> IoResult {
    let method = request_method_name(&client_request);
    let host_url_str = cfg.api.server.get_api_url();
    let host_url = Url::parse(&host_url_str).unwrap();

    let bw = BufferWriter::stdout(cfg.color_choice());
    let mut buf = bw.buffer();
    set_color(&mut buf, Color::Blue, true, false)?;
    write!(&mut buf, "{} ", method)?;
    set_color(&mut buf, Color::Cyan, true, false)?;
    write!(&mut buf, "/v1/{} ", client_request.route)?;
    set_color(&mut buf, Color::White, false, false)?;
    write!(&mut buf, "HTTP/1.1")?;
    buf.set_color(ColorSpec::new().set_reset(true))?;
    writeln!(&mut buf, "")?;

    fn write_header(buf: &mut Buffer, name: &str, value: &str, color: Color, intense: bool) -> IoResult {
        set_color(buf, Color::White, false, false)?;
        write!(buf, "{}: ", name)?;
        set_color(buf, color, intense, false)?;
        write!(buf, "{}", value)?;
        reset_color(buf)?;
        writeln!(buf, "")?;
        IoResult::Ok(())
    }

    write_header(&mut buf, "Host", &format!("{}:{}",
        host_url.host_str().unwrap(),
        host_url.port_or_known_default().unwrap()),
        Color::Cyan, true)?;
    for (name, value) in &client_request.headers {
        write_header(&mut buf, name, value, Color::Yellow, true)?;
    }
    if matches!(client_request.body, ClientRequestBody::Json(_)) {
        write_header(&mut buf, "Content-Type", "application/json", Color::Yellow, true)?;
    }
    match client_request.body {
        ClientRequestBody::None => {},
        ClientRequestBody::Json(data) => {
            writeln!(&mut buf, "")?;
            buf.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_intense(true))?;
            write!(&mut buf, "{}", pprint_json(&data))?;
            buf.set_color(ColorSpec::new().set_reset(true))?;
            writeln!(&mut buf, "")?;
        },
        ClientRequestBody::FileContent(path) => {
            writeln!(&mut buf, "")?;
            buf.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
            write!(&mut buf, "<FILE CONTENT: {}>", path.to_string_lossy())?;
            buf.set_color(ColorSpec::new().set_reset(true))?;
            writeln!(&mut buf, "")?;
        },
        ClientRequestBody::StdIn => {
            writeln!(&mut buf, "")?;
            buf.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
            write!(&mut buf, "<STDIN>")?;
            buf.set_color(ColorSpec::new().set_reset(true))?;
            writeln!(&mut buf, "")?;
        }
    }
    bw.print(&buf)?;
    IoResult::Ok(())
}

fn parent_exe() -> Option<String> {
    if let Ok(pid) = get_current_pid() {
        let mut sys = System::new();
        sys.refresh_processes_specifics(
            ProcessesToUpdate::Some(&[pid]), false, ProcessRefreshKind::nothing());
        if let Some(proc) = sys.process(pid) {
            if let Some(parent_pid) = proc.parent() {
                sys.refresh_processes_specifics(
                    ProcessesToUpdate::Some(&[parent_pid]), false, ProcessRefreshKind::nothing());
                if let Some(parent_proc) = sys.process(parent_pid) {
                    if let Some(parent_path) = parent_proc.exe() {
                        return Some(parent_path.to_string_lossy().to_string());
                    }
                }
            }
        }
    }
    return None;
}

fn is_in_cmd(parent: &str) -> bool {
    if parent.ends_with("cmd.exe") {
        return true;
    }
    return false;
}

fn is_in_powershell(parent: &str) -> bool {
    if parent.ends_with("powershell.exe") ||
        parent.ends_with("pwsh.exe") ||
        parent.ends_with("pwsh") {
        return true;
    }
    return false;
}

enum ParentShell {
    Unknown,
    PowerShell,
    Cmd,
}

fn get_parent_shell() -> ParentShell {
    if let Some(process_parent) = parent_exe() {
        if is_in_cmd(&process_parent) {
            return ParentShell::Cmd;
        }
        if is_in_powershell(&process_parent) {
            return ParentShell::PowerShell;
        }
    }
    return ParentShell::Unknown;
}

fn print_request_curl(cfg: &CliConfig, client_request: ClientRequest) -> IoResult {
    let bw = BufferWriter::stdout(cfg.color_choice());
    let mut buf = bw.buffer();

    fn flag(buf: &mut Buffer, name: &str, value: &str, color: Color, intense: bool) -> IoResult {
        set_color(buf, Color::White, false, false)?;
        write!(buf, " {}", name)?;
        set_color(buf, color, intense, false)?;
        write!(buf, " {}", value)?;
        reset_color(buf)?;
        Ok(())
    }

    reset_color(&mut buf)?;
    set_color(&mut buf, Color::White, true, false)?;
    write!(&mut buf, "curl")?;

    if !matches!(&client_request.method, ClientRequestMethod::GET) {
        let method = request_method_name(&client_request);
        flag(&mut buf, "-X", method, Color::Blue, true)?;
    }
    if let Some(timeout) = cfg.api.client.timeout {
        flag(&mut buf, "--connect-timeout", &timeout.to_string(), Color::White, false)?;
    }
    for (name, value) in &client_request.headers {
        flag(&mut buf, "-H", &format!("\"{}: {}\"", name, value), Color::Yellow, true)?;
    }
    match client_request.body {
        ClientRequestBody::None => {},
        ClientRequestBody::Json(data) => {
            flag(&mut buf, "-H", "\"Content-Type: application/json\"", Color::Yellow, true)?;
            let mut json = serde_json::to_string(&data).unwrap();
            match get_parent_shell() {
                ParentShell::Cmd => {
                    json = json.replace("\\\"", "\\\\\"");
                    json = json.replace('"', "\"\"");
                },
                ParentShell::PowerShell => {
                    json = json.replace('`', "``");
                    json = json.replace('"', "\\`\"");
                },
                _ => {
                    json = json.replace('\\', "\\\\");
                    json = json.replace('"', "\\\"");
                },
            }
            flag(&mut buf, "-d", &format!("\"{}\"", json), Color::Green, true)?;
        },
        ClientRequestBody::FileContent(path) => {
            flag(&mut buf, " --data-binary", &format!(" \"@{}\"", path.to_string_lossy()), Color::Green, false)?;
        },
        ClientRequestBody::StdIn => {
            flag(&mut buf, " --data-binary", " @-", Color::Green, false)?;
        },
    }
    let request_url = build_request_url(&cfg.api, &client_request.route);
    set_color(&mut buf, Color::Cyan, true, false)?;
    write!(&mut buf, " {}", request_url)?;
    reset_color(&mut buf)?;
    // end with newline
    writeln!(&mut buf, "")?;
    bw.print(&buf)?;
    IoResult::Ok(())
}

pub fn print_request(cfg: &CliConfig, r: ClientRequest) ->IoResult {
    if cfg.use_curl_syntax() {
        print_request_curl(cfg, r)
    } else {
        print_request_plain(cfg, r)
    }
}