#![allow(dead_code)]
use std::io::Write;
use anyhow::{Context, Result};
use clap::ValueEnum;
use serde::Serialize;
#[derive(Clone, Debug, Default, ValueEnum)]
pub enum OutputFormat {
#[default]
Table,
Json,
Yaml,
Yamls,
Jsonl,
}
pub trait JsonlSerialize {
fn write_jsonl(&self, out: &mut dyn Write) -> Result<()>;
}
pub fn write_items_jsonl<'a, I, T>(items: I, out: &mut dyn Write) -> Result<()>
where
I: IntoIterator<Item = &'a T>,
T: Serialize + 'a,
{
for item in items {
let line = serde_json::to_string(item).context("Failed to serialize as JSON")?;
writeln!(out, "{line}").context("Failed to write JSONL line")?;
}
Ok(())
}
pub fn write_scalar_jsonl<T: Serialize>(item: &T, out: &mut dyn Write) -> Result<()> {
let line = serde_json::to_string(item).context("Failed to serialize as JSON")?;
writeln!(out, "{line}").context("Failed to write JSONL line")?;
Ok(())
}
impl<T: Serialize> JsonlSerialize for Vec<T> {
fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
write_items_jsonl(self.iter(), out)
}
}
pub fn write_output<T: Serialize + JsonlSerialize>(
data: &T,
format: &OutputFormat,
out: &mut dyn Write,
) -> Result<bool> {
match format {
OutputFormat::Table => Ok(false),
OutputFormat::Json => {
let rendered =
serde_json::to_string_pretty(data).context("Failed to serialize as JSON")?;
writeln!(out, "{rendered}").context("Failed to write JSON output")?;
Ok(true)
}
OutputFormat::Yaml => {
let rendered = serde_yaml::to_string(data).context("Failed to serialize as YAML")?;
write!(out, "{rendered}").context("Failed to write YAML output")?;
Ok(true)
}
OutputFormat::Yamls => {
let rendered = format_yaml_stream(data)?;
write!(out, "{rendered}").context("Failed to write YAML stream output")?;
Ok(true)
}
OutputFormat::Jsonl => {
data.write_jsonl(out)?;
Ok(true)
}
}
}
fn yaml_stream_doc(value: &serde_yaml::Value) -> Result<String> {
let s = serde_yaml::to_string(value).context("Failed to serialize YAML stream item")?;
Ok(format!("---\n{s}"))
}
fn format_yaml_stream<T: Serialize>(data: &T) -> Result<String> {
match serde_yaml::to_value(data).context("Failed to serialize as YAML stream")? {
serde_yaml::Value::Sequence(items) => items.iter().map(yaml_stream_doc).collect(),
other => yaml_stream_doc(&other),
}
}
pub fn output_as<T: Serialize + JsonlSerialize>(data: &T, format: &OutputFormat) -> Result<bool> {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
write_output(data, format, &mut handle)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn output_default_is_table() {
assert!(matches!(OutputFormat::default(), OutputFormat::Table));
}
#[test]
fn output_debug_format() {
assert_eq!(format!("{:?}", OutputFormat::Jsonl), "Jsonl");
}
#[test]
fn output_clone() {
let f = OutputFormat::Json.clone();
assert!(matches!(f, OutputFormat::Json));
}
#[test]
fn write_output_table_returns_false_and_writes_nothing() {
let data = vec![1_i32, 2];
let mut buf = Vec::new();
let wrote = write_output(&data, &OutputFormat::Table, &mut buf).unwrap();
assert!(!wrote);
assert!(buf.is_empty());
}
#[test]
fn write_output_json_emits_pretty_array() {
let data = vec![1_i32, 2, 3];
let mut buf = Vec::new();
let wrote = write_output(&data, &OutputFormat::Json, &mut buf).unwrap();
assert!(wrote);
let out = String::from_utf8(buf).unwrap();
assert!(out.starts_with('['));
assert!(out.ends_with("]\n"));
}
#[test]
fn write_output_yaml_emits_list() {
let data = vec![1_i32, 2];
let mut buf = Vec::new();
write_output(&data, &OutputFormat::Yaml, &mut buf).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "- 1\n- 2\n");
}
#[test]
fn write_output_yamls_emits_stream() {
let data = vec![1_i32, 2];
let mut buf = Vec::new();
write_output(&data, &OutputFormat::Yamls, &mut buf).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "---\n1\n---\n2\n");
}
#[test]
fn write_output_jsonl_emits_one_line_per_item() {
let data = vec![1_i32, 2, 3];
let mut buf = Vec::new();
write_output(&data, &OutputFormat::Jsonl, &mut buf).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "1\n2\n3\n");
}
#[test]
fn output_as_table_returns_false() {
let data: Vec<i32> = vec![];
assert!(!output_as(&data, &OutputFormat::Table).unwrap());
}
#[test]
fn output_as_json_returns_true() {
let data: Vec<i32> = vec![];
assert!(output_as(&data, &OutputFormat::Json).unwrap());
}
#[test]
fn write_items_jsonl_over_slice() {
let data = [10_i32, 20];
let mut buf = Vec::new();
write_items_jsonl(data.iter(), &mut buf).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "10\n20\n");
}
#[test]
fn write_scalar_jsonl_emits_one_line() {
#[derive(Serialize)]
struct Scalar {
name: &'static str,
}
let mut buf = Vec::new();
write_scalar_jsonl(&Scalar { name: "solo" }, &mut buf).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "{\"name\":\"solo\"}\n");
}
#[test]
fn yaml_stream_non_sequence_emits_single_doc() {
#[derive(Serialize)]
struct S {
key: &'static str,
}
let out = format_yaml_stream(&S { key: "v" }).unwrap();
assert_eq!(out, "---\nkey: v\n");
}
}