use std::io::Write;
use serde_json::Value;
use crate::errors;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Format {
#[default]
Text,
Json,
JsonL,
Markdown,
}
impl Format {
pub fn parse(s: &str) -> Option<Format> {
match s {
"" | "text" => Some(Format::Text),
"json" => Some(Format::Json),
"jsonl" => Some(Format::JsonL),
"markdown" => Some(Format::Markdown),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Format::Text => "text",
Format::Json => "json",
Format::JsonL => "jsonl",
Format::Markdown => "markdown",
}
}
pub fn is_machine(&self) -> bool {
matches!(self, Format::Json | Format::JsonL)
}
}
pub fn render_jsonl(w: &mut dyn Write, v: &Value) -> errors::Result<()> {
match v {
Value::Null => Ok(()),
Value::Array(items) => {
for item in items {
writeln!(
w,
"{}",
serde_json::to_string(item).unwrap_or_else(|_| "null".into())
)?;
}
Ok(())
}
other => {
writeln!(
w,
"{}",
serde_json::to_string(other).unwrap_or_else(|_| "null".into())
)?;
Ok(())
}
}
}
pub fn render_markdown(w: &mut dyn Write, v: &Value) -> errors::Result<()> {
match v {
Value::Null => Ok(()),
Value::String(_) | Value::Bool(_) | Value::Number(_) => {
writeln!(w, "{}", md_escape(&cell(v)))?;
Ok(())
}
Value::Object(o) => md_kv(w, o),
Value::Array(items) => {
if items.is_empty() {
return Ok(());
}
if items[0].is_object() {
md_table(w, items)
} else {
for item in items {
writeln!(w, "- {}", md_escape(&cell(item)))?;
}
Ok(())
}
}
}
}
fn md_kv(w: &mut dyn Write, o: &serde_json::Map<String, Value>) -> errors::Result<()> {
if o.is_empty() {
return Ok(());
}
writeln!(w, "| Field | Value |")?;
writeln!(w, "| --- | --- |")?;
for (k, val) in o {
writeln!(w, "| {} | {} |", md_escape(k), md_escape(&cell(val)))?;
}
Ok(())
}
fn md_table(w: &mut dyn Write, items: &[Value]) -> errors::Result<()> {
let Some(first) = items[0].as_object() else {
return Ok(());
};
let keys: Vec<&String> = first.keys().collect();
if keys.is_empty() {
return Ok(());
}
write!(w, "|")?;
for k in &keys {
write!(w, " {} |", md_escape(k))?;
}
writeln!(w)?;
write!(w, "|")?;
for _ in &keys {
write!(w, " --- |")?;
}
writeln!(w)?;
for item in items {
write!(w, "|")?;
let row = item.as_object();
for k in &keys {
let val = row.and_then(|r| r.get(k.as_str()));
let text = match val {
Some(v) => md_escape(&cell(v)),
None => String::new(),
};
write!(w, " {text} |")?;
}
writeln!(w)?;
}
Ok(())
}
fn cell(v: &Value) -> String {
crate::cli::render::format_cell(v)
}
fn md_escape(s: &str) -> String {
s.replace("\r\n", "<br>")
.replace('\n', "<br>")
.replace('|', "\\|")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn md(v: &Value) -> String {
let mut buf = Vec::new();
render_markdown(&mut buf, v).unwrap();
String::from_utf8(buf).unwrap()
}
fn jsonl(v: &Value) -> String {
let mut buf = Vec::new();
render_jsonl(&mut buf, v).unwrap();
String::from_utf8(buf).unwrap()
}
#[test]
fn jsonl_shapes() {
assert_eq!(jsonl(&json!(null)), "");
assert_eq!(jsonl(&json!([1, 2])), "1\n2\n");
assert_eq!(jsonl(&json!({"a": 1})), "{\"a\":1}\n");
assert_eq!(jsonl(&json!("x")), "\"x\"\n");
}
#[test]
fn markdown_shapes() {
assert_eq!(md(&json!(null)), "");
assert_eq!(md(&json!("hi")), "hi\n");
assert_eq!(
md(&json!({"k": "v", "long": 1})),
"| Field | Value |\n| --- | --- |\n| k | v |\n| long | 1 |\n"
);
assert_eq!(md(&json!([1, "a"])), "- 1\n- a\n");
assert_eq!(
md(&json!([{"a": 1, "b": 2}, {"a": 3, "b": 4}])),
"| a | b |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |\n"
);
assert_eq!(
md(&json!({"k": "a|b\nc"})),
"| Field | Value |\n| --- | --- |\n| k | a\\|b<br>c |\n"
);
assert_eq!(md(&json!({})), "");
assert_eq!(md(&json!([])), "");
}
#[test]
fn format_parsing() {
assert_eq!(Format::parse(""), Some(Format::Text));
assert_eq!(Format::parse("jsonl"), Some(Format::JsonL));
assert_eq!(Format::parse("MARKDOWN"), None);
assert!(Format::Json.is_machine() && !Format::Markdown.is_machine());
}
}