use serde_json::Value;
#[derive(Clone, Copy)]
pub enum Format {
Compact,
Pretty,
Table,
}
pub fn print(value: &Value, format: Format) {
println!("{}", render(value, format));
}
pub fn fail(err: &anyhow::Error, format: Format) -> ! {
fail_with_code(&err.to_string(), 1, format)
}
pub fn fail_with_code(message: &str, code: i32, format: Format) -> ! {
let value = serde_json::json!({ "error": message });
eprintln!("{}", render(&value, format));
std::process::exit(code);
}
fn render(value: &Value, format: Format) -> String {
match format {
Format::Compact => serde_json::to_string(value).expect("value is always serializable"),
Format::Pretty => {
serde_json::to_string_pretty(value).expect("value is always serializable")
}
Format::Table => render_table(value),
}
}
#[allow(clippy::option_if_let_else)]
fn render_table(value: &Value) -> String {
match value {
Value::Array(items) if items.is_empty() => "(no results)".to_string(),
Value::Array(items) => match items[0].as_object() {
Some(first_obj) => {
let headers: Vec<String> = first_obj.keys().cloned().collect();
let mut rows = vec![headers.iter().map(|h| h.to_uppercase()).collect()];
for item in items {
let obj = item.as_object();
rows.push(
headers
.iter()
.map(|h| obj.and_then(|o| o.get(h)).map(cell).unwrap_or_default())
.collect(),
);
}
render_rows(&rows)
}
None => items.iter().map(cell).collect::<Vec<_>>().join("\n"),
},
Value::Object(map) => {
let mut rows = vec![vec!["FIELD".to_string(), "VALUE".to_string()]];
for (k, v) in map {
rows.push(vec![k.clone(), cell(v)]);
}
render_rows(&rows)
}
other => cell(other),
}
}
fn cell(value: &Value) -> String {
match value {
Value::Null => String::new(),
Value::String(s) => s.clone(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
other => other.to_string(),
}
}
fn render_rows(rows: &[Vec<String>]) -> String {
let num_cols = rows.first().map_or(0, Vec::len);
let mut widths = vec![0usize; num_cols];
for row in rows {
for (i, cell) in row.iter().enumerate() {
widths[i] = widths[i].max(cell.chars().count());
}
}
rows.iter()
.map(|row| {
row.iter()
.enumerate()
.map(|(i, cell)| format!("{cell:<width$}", width = widths[i]))
.collect::<Vec<_>>()
.join(" ")
.trim_end()
.to_string()
})
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn render_table_array_of_objects_becomes_aligned_table() {
let value = json!([
{"id": 1, "name": "alice"},
{"id": 2, "name": "bob"},
]);
let table = render_table(&value);
assert!(table.contains("ID"));
assert!(table.contains("NAME"));
assert!(table.contains("alice"));
assert!(table.contains("bob"));
}
#[test]
fn render_table_empty_array_is_friendly_message() {
assert_eq!(render_table(&json!([])), "(no results)");
}
#[test]
fn render_table_single_object_becomes_field_value_table() {
let value = json!({"error": "task 1 not found"});
let table = render_table(&value);
assert!(table.contains("FIELD"));
assert!(table.contains("VALUE"));
assert!(table.contains("error"));
assert!(table.contains("task 1 not found"));
}
#[test]
fn render_table_array_of_scalars_renders_line_by_line() {
let value = json!(["alpha", "beta"]);
assert_eq!(render_table(&value), "alpha\nbeta");
}
#[test]
fn render_table_bare_top_level_scalar_falls_through_to_cell() {
assert_eq!(render_table(&json!("just a string")), "just a string");
assert_eq!(render_table(&json!(42)), "42");
}
#[test]
fn cell_renders_each_scalar_type() {
assert_eq!(cell(&Value::Null), "");
assert_eq!(cell(&json!("text")), "text");
assert_eq!(cell(&json!(42)), "42");
assert_eq!(cell(&json!(true)), "true");
assert_eq!(cell(&json!(false)), "false");
}
#[test]
fn cell_renders_nested_array_or_object_as_inline_compact_json() {
assert_eq!(cell(&json!(["a", "b"])), "[\"a\",\"b\"]");
assert_eq!(cell(&json!({"k": "v"})), "{\"k\":\"v\"}");
}
#[test]
fn render_rows_pads_columns_to_widest_cell() {
let rows = vec![
vec!["ID".to_string(), "NAME".to_string()],
vec!["1".to_string(), "alice".to_string()],
vec!["22".to_string(), "b".to_string()],
];
let rendered = render_rows(&rows);
let lines: Vec<&str> = rendered.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0].find("NAME"), lines[1].find("alice"));
assert_eq!(lines[0].find("NAME"), lines[2].find('b'));
}
}