revault_cli 0.0.3

CLI for reVault encrypted lockboxes, store files, variables and forms with integration to your platform key storage
use clap::ArgMatches;
use revault_lockbox_api::Error;

use super::context::CliResult;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum OutputFormat {
    Table,
    Tsv,
    Json,
}

impl OutputFormat {
    pub(crate) fn parse(value: Option<&str>) -> CliResult<Self> {
        match value.unwrap_or("table") {
            "table" => Ok(Self::Table),
            "tsv" => Ok(Self::Tsv),
            "json" => Ok(Self::Json),
            value => Err(Error::InvalidInput(format!(
                "unsupported output format: {value}; expected table, tsv, or json"
            ))
            .into()),
        }
    }
}

pub(crate) fn output_format_from_matches(matches: &ArgMatches) -> CliResult<OutputFormat> {
    OutputFormat::parse(matches.get_one::<String>("format").map(String::as_str))
}

pub(crate) fn print_records(
    headers: &[&str],
    rows: Vec<Vec<String>>,
    format: OutputFormat,
) -> CliResult<()> {
    if rows.is_empty() {
        println!("empty");
        return Ok(());
    }
    match format {
        OutputFormat::Table => print_table(headers, &rows),
        OutputFormat::Tsv => print_tsv(&rows),
        OutputFormat::Json => print_json(headers, &rows)?,
    }
    Ok(())
}

fn print_table(headers: &[&str], rows: &[Vec<String>]) {
    let mut widths = headers
        .iter()
        .map(|header| header.len())
        .collect::<Vec<_>>();
    for row in rows {
        for (index, value) in row.iter().enumerate() {
            if let Some(width) = widths.get_mut(index) {
                *width = (*width).max(value.len());
            }
        }
    }
    print_table_row(
        &headers
            .iter()
            .map(|value| (*value).to_string())
            .collect::<Vec<_>>(),
        &widths,
    );
    for row in rows {
        print_table_row(row, &widths);
    }
}

fn print_table_row(row: &[String], widths: &[usize]) {
    for (index, value) in row.iter().enumerate() {
        if index > 0 {
            print!("  ");
        }
        let width = widths.get(index).copied().unwrap_or(value.len());
        print!("{value:<width$}");
    }
    println!();
}

fn print_tsv(rows: &[Vec<String>]) {
    for row in rows {
        println!("{}", row.join("\t"));
    }
}

fn print_json(headers: &[&str], rows: &[Vec<String>]) -> CliResult<()> {
    for row in rows {
        if row.len() != headers.len() {
            return Err(Error::InvalidInput("output row/header width mismatch".to_string()).into());
        }
        let fields = headers
            .iter()
            .zip(row.iter())
            .map(|(name, value)| format!("{}:{}", json_string(name), json_string(value)))
            .collect::<Vec<_>>();
        println!("{{{}}}", fields.join(","));
    }
    Ok(())
}

pub(crate) fn json_string(value: &str) -> String {
    serde_json::to_string(value).expect("serializing a string to JSON cannot fail")
}