use std::fmt::Write as _;
use clap::ValueEnum;
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum Format {
#[default]
Human,
Tsv,
Json,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FormatChoice {
pub json: bool,
pub tsv: bool,
}
impl FormatChoice {
pub fn resolve(self) -> Format {
if self.json {
Format::Json
} else if self.tsv {
Format::Tsv
} else {
Format::Human
}
}
}
pub fn render_table(headers: &[&str], rows: &[Vec<String>], fmt: Format) -> String {
if rows.is_empty() && matches!(fmt, Format::Human) {
return String::from("(no rows)\n");
}
match fmt {
Format::Tsv => {
rows.iter()
.map(|r| r.join("\t"))
.collect::<Vec<_>>()
.join("\n")
.trim_end()
.to_string()
+ "\n"
}
Format::Json => {
let arr: Vec<Value> = rows
.iter()
.map(|r| {
let mut m = serde_json::Map::new();
for (i, h) in headers.iter().enumerate() {
m.insert((*h).to_string(), Value::String(r[i].clone()));
}
Value::Object(m)
})
.collect();
serde_json::to_string_pretty(&arr).unwrap_or_default() + "\n"
}
Format::Human => {
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
if cell.len() > widths[i] {
widths[i] = cell.len();
}
}
}
let mut out = String::new();
for (i, h) in headers.iter().enumerate() {
let _ = write!(out, "{:<width$}", h, width = widths[i]);
if i + 1 < headers.len() {
out.push_str(" ");
}
}
out.push('\n');
for row in rows {
for (i, cell) in row.iter().enumerate() {
let _ = write!(out, "{:<width$}", cell, width = widths[i]);
if i + 1 < row.len() {
out.push_str(" ");
}
}
out.push('\n');
}
out
}
}
}
pub fn render_record(pairs: &[(&str, String)], fmt: Format) -> String {
match fmt {
Format::Json => {
let mut m = serde_json::Map::new();
for (k, v) in pairs {
m.insert((*k).to_string(), Value::String(v.clone()));
}
serde_json::to_string_pretty(&Value::Object(m)).unwrap_or_default() + "\n"
}
Format::Tsv => {
pairs
.iter()
.map(|(_, v)| v.as_str())
.collect::<Vec<_>>()
.join("\t")
+ "\n"
}
Format::Human => {
let key_w = pairs.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
let mut out = String::new();
for (k, v) in pairs {
let _ = writeln!(out, "{:<key_w$} {}", k, v, key_w = key_w);
}
out
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_table_pads_columns() {
let s = render_table(
&["a", "bbb"],
&[
vec!["1".into(), "two".into()],
vec!["12".into(), "x".into()],
],
Format::Human,
);
let lines: Vec<&str> = s.lines().collect();
assert_eq!(lines[0], "a bbb");
assert_eq!(lines[1], "1 two");
assert_eq!(lines[2], "12 x ");
}
#[test]
fn tsv_table_uses_tabs() {
let s = render_table(&["a", "b"], &[vec!["1".into(), "2".into()]], Format::Tsv);
assert_eq!(s, "1\t2\n");
}
#[test]
fn json_table_is_array_of_objects() {
let s = render_table(&["a", "b"], &[vec!["1".into(), "2".into()]], Format::Json);
let v: Value = serde_json::from_str(s.trim()).unwrap();
assert_eq!(v[0]["a"], "1");
assert_eq!(v[0]["b"], "2");
}
#[test]
fn empty_human_table_is_marker() {
let s = render_table(&["a"], &[], Format::Human);
assert_eq!(s, "(no rows)\n");
}
#[test]
fn format_choice_priority() {
assert_eq!(
FormatChoice {
json: true,
tsv: true
}
.resolve(),
Format::Json
);
assert_eq!(
FormatChoice {
json: false,
tsv: true
}
.resolve(),
Format::Tsv
);
assert_eq!(FormatChoice::default().resolve(), Format::Human);
}
}