use clap::ValueEnum;
use serde::Serialize;
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum OutputFormat {
#[default]
Json,
Ndjson,
Table,
Quiet,
}
pub struct OutputHandler {
format: OutputFormat,
quiet: bool,
}
impl OutputHandler {
pub fn new(format: OutputFormat) -> Self {
Self {
format,
quiet: matches!(format, OutputFormat::Quiet),
}
}
pub fn output<T: Serialize>(&self, value: &T) -> anyhow::Result<()> {
if self.quiet {
return Ok(());
}
match self.format {
OutputFormat::Json => {
let json = serde_json::to_string_pretty(value)?;
println!("{}", json);
}
OutputFormat::Ndjson => {
let json = serde_json::to_string(value)?;
println!("{}", json);
}
OutputFormat::Table => {
let json = serde_json::to_string_pretty(value)?;
println!("{}", json);
}
OutputFormat::Quiet => {}
}
Ok(())
}
pub fn output_raw(&self, s: &str) {
if !self.quiet {
println!("{}", s);
}
}
}
pub fn format_table(headers: &[&str], rows: &[Vec<String>]) -> String {
use tabled::Tabled;
#[derive(Tabled)]
#[allow(dead_code)]
struct TableRow {
#[tabled(skip)]
_dummy: (),
}
if rows.is_empty() {
return "No data to display".to_string();
}
let mut output = String::new();
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(cell.len());
}
}
}
let header_row: Vec<String> = headers
.iter()
.enumerate()
.map(|(i, h)| format!("{:width$}", h, width = widths.get(i).unwrap_or(&10)))
.collect();
output.push_str(&header_row.join(" | "));
output.push('\n');
let separator: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
output.push_str(&separator.join("-+-"));
output.push('\n');
for row in rows {
let formatted_row: Vec<String> = row
.iter()
.enumerate()
.map(|(i, cell)| format!("{:width$}", cell, width = widths.get(i).unwrap_or(&10)))
.collect();
output.push_str(&formatted_row.join(" | "));
output.push('\n');
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_output_handler_json_output() {
let handler = OutputHandler::new(OutputFormat::Json);
let data = json!({"name": "test", "value": 42});
let result = handler.output(&data);
assert!(result.is_ok());
}
#[test]
fn test_output_handler_ndjson_output() {
let handler = OutputHandler::new(OutputFormat::Ndjson);
let data = json!({"name": "test", "value": 42});
let result = handler.output(&data);
assert!(result.is_ok());
}
#[test]
fn test_output_handler_table_output() {
let handler = OutputHandler::new(OutputFormat::Table);
let data = json!({"name": "test", "value": 42});
let result = handler.output(&data);
assert!(result.is_ok());
}
#[test]
fn test_output_handler_quiet_mode() {
let handler = OutputHandler::new(OutputFormat::Quiet);
let data = json!({"name": "test", "value": 42});
let result = handler.output(&data);
assert!(result.is_ok());
}
#[test]
fn test_output_handler_output_raw() {
let handler = OutputHandler::new(OutputFormat::Json);
handler.output_raw("Raw text output");
}
#[test]
fn test_output_handler_output_raw_quiet() {
let handler = OutputHandler::new(OutputFormat::Quiet);
handler.output_raw("This should not appear");
}
#[test]
fn test_output_format_variants() {
assert!(matches!(OutputFormat::Json, OutputFormat::Json));
assert!(matches!(OutputFormat::Ndjson, OutputFormat::Ndjson));
assert!(matches!(OutputFormat::Table, OutputFormat::Table));
assert!(matches!(OutputFormat::Quiet, OutputFormat::Quiet));
let default: OutputFormat = Default::default();
assert!(matches!(default, OutputFormat::Json));
}
#[test]
fn test_format_table_empty() {
let headers = vec!["Name", "Value"];
let rows: Vec<Vec<String>> = vec![];
let result = format_table(&headers, &rows);
assert_eq!(result, "No data to display");
}
#[test]
fn test_format_table_single_row() {
let headers = vec!["Name", "Value"];
let rows = vec![vec!["Test".to_string(), "100".to_string()]];
let result = format_table(&headers, &rows);
assert!(result.contains("Name"));
assert!(result.contains("Value"));
assert!(result.contains("Test"));
assert!(result.contains("100"));
assert!(result.lines().count() >= 3);
}
#[test]
fn test_format_table_multiple_rows() {
let headers = vec!["ID", "Name"];
let rows = vec![
vec!["1".to_string(), "First".to_string()],
vec!["2".to_string(), "Second".to_string()],
vec!["3".to_string(), "Third".to_string()],
];
let result = format_table(&headers, &rows);
assert!(result.contains("ID"));
assert!(result.contains("Name"));
assert!(result.contains("1"));
assert!(result.contains("First"));
assert!(result.contains("2"));
assert!(result.contains("Second"));
assert!(result.contains("3"));
assert!(result.contains("Third"));
}
#[test]
fn test_format_table_with_alignment() {
let headers = vec!["Short", "VeryLongHeader"];
let rows = vec![vec!["A".to_string(), "B".to_string()]];
let result = format_table(&headers, &rows);
assert!(result.contains("Short"));
assert!(result.contains("VeryLongHeader"));
}
}