Files
cli/sunbeam-sdk/src/output.rs
Sienna Meridian Satterwhite 3d7a2d5d34 feat: OutputFormat enum + render/render_list/read_json_input helpers
Adds -o json|yaml|table output support to the SDK output module.
OutputFormat derives clap::ValueEnum behind the cli feature gate.
2026-03-21 22:17:10 +00:00

194 lines
5.4 KiB
Rust

use crate::error::{Result, SunbeamError};
use serde::Serialize;
// ---------------------------------------------------------------------------
// OutputFormat
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
pub enum OutputFormat {
#[default]
Table,
Json,
Yaml,
}
/// Render a single serialisable value in the requested format.
pub fn render<T: Serialize>(val: &T, format: OutputFormat) -> Result<()> {
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(val)?);
}
OutputFormat::Yaml => {
print!("{}", serde_yaml::to_string(val)?);
}
OutputFormat::Table => {
// Fallback: pretty JSON when no table renderer is provided
println!("{}", serde_json::to_string_pretty(val)?);
}
}
Ok(())
}
/// Render a list of items as table / json / yaml.
///
/// `to_row` converts each item into a `Vec<String>` of column values matching
/// the order of `headers`.
pub fn render_list<T: Serialize>(
rows: &[T],
headers: &[&str],
to_row: fn(&T) -> Vec<String>,
format: OutputFormat,
) -> Result<()> {
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(rows)?);
}
OutputFormat::Yaml => {
print!("{}", serde_yaml::to_string(rows)?);
}
OutputFormat::Table => {
let table_rows: Vec<Vec<String>> = rows.iter().map(to_row).collect();
println!("{}", table(table_rows.as_slice(), headers));
}
}
Ok(())
}
/// Read JSON input from a `--data` flag value or stdin when the value is `"-"`.
pub fn read_json_input(flag: Option<&str>) -> Result<serde_json::Value> {
let raw = match flag {
Some("-") | None => {
let mut buf = String::new();
std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)?;
buf
}
Some(v) => v.to_string(),
};
serde_json::from_str(&raw)
.map_err(|e| SunbeamError::Other(format!("invalid JSON input: {e}")))
}
// ---------------------------------------------------------------------------
// Existing helpers
// ---------------------------------------------------------------------------
/// Print a step header.
pub fn step(msg: &str) {
println!("\n==> {msg}");
}
/// Print a success/info line.
pub fn ok(msg: &str) {
println!(" {msg}");
}
/// Print a warning to stderr.
pub fn warn(msg: &str) {
eprintln!(" WARN: {msg}");
}
/// Return an aligned text table. Columns padded to max width.
pub fn table(rows: &[Vec<String>], headers: &[&str]) -> String {
if headers.is_empty() {
return String::new();
}
let mut col_widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
if i < col_widths.len() {
col_widths[i] = col_widths[i].max(cell.len());
}
}
}
let header_line: String = headers
.iter()
.enumerate()
.map(|(i, h)| format!("{:<width$}", h, width = col_widths[i]))
.collect::<Vec<_>>()
.join(" ");
let separator: String = col_widths
.iter()
.map(|&w| "-".repeat(w))
.collect::<Vec<_>>()
.join(" ");
let mut lines = vec![header_line, separator];
for row in rows {
let cells: Vec<String> = (0..headers.len())
.map(|i| {
let val = row.get(i).map(|s| s.as_str()).unwrap_or("");
format!("{:<width$}", val, width = col_widths[i])
})
.collect();
lines.push(cells.join(" "));
}
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_table_basic() {
let rows = vec![
vec!["abc".to_string(), "def".to_string()],
vec!["x".to_string(), "longer".to_string()],
];
let result = table(&rows, &["Col1", "Col2"]);
assert!(result.contains("Col1"));
assert!(result.contains("Col2"));
assert!(result.contains("abc"));
assert!(result.contains("longer"));
}
#[test]
fn test_table_empty_headers() {
let result = table(&[], &[]);
assert!(result.is_empty());
}
#[test]
fn test_table_column_widths() {
let rows = vec![vec!["short".to_string(), "x".to_string()]];
let result = table(&rows, &["LongHeader", "H2"]);
for line in result.lines().skip(2) {
assert!(line.starts_with("short "));
}
}
#[test]
fn test_render_json() {
let val = serde_json::json!({"key": "value"});
// Just ensure it doesn't panic
render(&val, OutputFormat::Json).unwrap();
}
#[test]
fn test_render_yaml() {
let val = serde_json::json!({"key": "value"});
render(&val, OutputFormat::Yaml).unwrap();
}
#[test]
fn test_render_list_table() {
#[derive(Serialize)]
struct Item { name: String }
let items = vec![Item { name: "test".into() }];
render_list(&items, &["NAME"], |i| vec![i.name.clone()], OutputFormat::Table).unwrap();
}
#[test]
fn test_read_json_input_inline() {
let val = read_json_input(Some(r#"{"a":1}"#)).unwrap();
assert_eq!(val["a"], 1);
}
}