use std::io::Write;
use serde::Serialize;
use serde_json::Value;
use crate::errors;
pub fn render<R: Serialize>(w: &mut dyn Write, v: &R) -> errors::Result<()> {
let value = serde_json::to_value(v).map_err(|e| {
errors::Error::new(errors::Kind::Internal, format!("result serialization: {e}"))
})?;
render_value(w, &value)
}
pub fn render_value(w: &mut dyn Write, v: &Value) -> errors::Result<()> {
match v {
Value::Null => Ok(()),
Value::String(s) => {
writeln!(w, "{s}")?;
Ok(())
}
Value::Bool(b) => {
writeln!(w, "{b}")?;
Ok(())
}
Value::Number(n) => {
writeln!(w, "{}", fmt_number(n))?;
Ok(())
}
Value::Array(items) => {
if items.is_empty() {
return Ok(());
}
if items.first().map(Value::is_object).unwrap_or(false) {
render_table(w, items)
} else {
for item in items {
writeln!(w, "{}", format_cell(item))?;
}
Ok(())
}
}
Value::Object(o) => {
if let Some(blocks) = crate::blocks::extract(v) {
return render_blocks(w, &blocks);
}
render_kv(w, o)
}
}
}
fn render_blocks(w: &mut dyn Write, blocks: &[crate::blocks::Block]) -> errors::Result<()> {
for b in blocks {
match b {
crate::blocks::Block::Text { text } => writeln!(w, "{text}")?,
crate::blocks::Block::Image { mime_type, data }
| crate::blocks::Block::Audio { mime_type, data } => {
let path = spill_to(&std::env::temp_dir(), mime_type, data)?;
writeln!(w, "{}", path.display())?;
}
}
}
Ok(())
}
pub fn spill_to(
dir: &std::path::Path,
mime_type: &str,
data: &str,
) -> errors::Result<std::path::PathBuf> {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let bytes = crate::blocks::decode_base64(data)?;
let ext = crate::blocks::ext_for_mime(mime_type);
let name = format!(
"xyz-blk-{}-{}.{ext}",
std::process::id(),
SEQ.fetch_add(1, Ordering::Relaxed)
);
let path = dir.join(name);
std::fs::write(&path, bytes)?;
Ok(path)
}
fn fmt_number(n: &serde_json::Number) -> String {
if let Some(f) = n.as_f64()
&& n.as_i64().is_none()
&& n.as_u64().is_none()
{
return f.to_string();
}
n.to_string()
}
fn render_kv(w: &mut dyn Write, o: &serde_json::Map<String, Value>) -> errors::Result<()> {
if o.is_empty() {
return Ok(());
}
let width = o.keys().map(|k| k.len()).max().unwrap_or(0);
for (k, v) in o {
writeln!(w, "{k:<width$} {}", format_cell(v))?;
}
Ok(())
}
fn render_table(w: &mut dyn Write, items: &[Value]) -> errors::Result<()> {
let first = match items.first().and_then(Value::as_object) {
Some(o) => o,
None => return Ok(()),
};
let keys: Vec<&String> = first.keys().collect();
if keys.is_empty() {
return Ok(());
}
const MAX_PAD: usize = 200;
let cell_width = |c: &str| c.chars().count().min(MAX_PAD);
let mut widths: Vec<usize> = keys.iter().map(|k| cell_width(k)).collect();
let mut rows: Vec<Vec<String>> = Vec::with_capacity(items.len());
for item in items {
let mut row = Vec::with_capacity(keys.len());
match item.as_object() {
Some(o) => {
for (j, k) in keys.iter().enumerate() {
let cell = o.get(*k).map(format_cell).unwrap_or_default();
widths[j] = widths[j].max(cell_width(&cell));
row.push(cell);
}
}
None => {
for k in keys.iter() {
row.push(format_cell(item));
let _ = k;
}
}
}
rows.push(row);
}
write_row(
w,
&keys.iter().map(|k| k.to_string()).collect::<Vec<_>>(),
&widths,
)?;
let dashes: Vec<String> = widths.iter().map(|wd| "-".repeat(*wd)).collect();
write_row(w, &dashes, &widths)?;
for row in &rows {
write_row(w, row, &widths)?;
}
Ok(())
}
fn write_row(w: &mut dyn Write, cells: &[String], widths: &[usize]) -> errors::Result<()> {
for (i, c) in cells.iter().enumerate() {
if i > 0 {
write!(w, " ")?;
}
write!(w, "{c:<width$}", width = widths[i])?;
}
writeln!(w)?;
Ok(())
}
pub fn format_cell(v: &Value) -> String {
match v {
Value::Null => String::new(),
Value::String(s) => s.clone(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => fmt_number(n),
Value::Array(items) => {
let inner = items.iter().map(format_cell).collect::<Vec<_>>().join(" ");
format!("[{inner}]")
}
Value::Object(o) => serde_json::to_string(o).unwrap_or_else(|_| "{}".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn render(v: &Value) -> String {
let mut out = Vec::new();
render_value(&mut out, v).unwrap();
String::from_utf8(out).unwrap()
}
#[test]
fn long_cells_do_not_panic_and_pad_is_capped() {
let long = "x".repeat(70_000);
let v = serde_json::json!([
{"a": long.clone(), "b": "y"},
{"a": "short", "b": "z"},
]);
let text = render(&v);
assert!(text.contains(&long), "long cell content must be preserved");
let second = text.lines().nth(3).unwrap();
assert!(
second.len() < 1000,
"row padded to the long cell width: {} bytes",
second.len()
);
}
#[test]
fn cjk_cells_use_char_widths() {
let v = serde_json::json!([
{"name": "中文", "n": 1},
{"name": "ab", "n": 2},
]);
let text = render(&v);
let rule = text.lines().nth(1).unwrap();
assert!(rule.starts_with("----"), "{rule}");
assert_eq!(rule.split(" ").next().unwrap().chars().count(), 4);
}
}