1use std::io::Write;
18
19use serde::Serialize;
20use serde_json::Value;
21
22use crate::errors;
23
24pub fn render<R: Serialize>(w: &mut dyn Write, v: &R) -> errors::Result<()> {
26 let value = serde_json::to_value(v).map_err(|e| {
27 errors::Error::new(errors::Kind::Internal, format!("result serialization: {e}"))
28 })?;
29 render_value(w, &value)
30}
31
32pub fn render_value(w: &mut dyn Write, v: &Value) -> errors::Result<()> {
34 match v {
35 Value::Null => Ok(()),
36 Value::String(s) => {
37 writeln!(w, "{s}")?;
38 Ok(())
39 }
40 Value::Bool(b) => {
41 writeln!(w, "{b}")?;
42 Ok(())
43 }
44 Value::Number(n) => {
45 writeln!(w, "{}", fmt_number(n))?;
46 Ok(())
47 }
48 Value::Array(items) => {
49 if items.is_empty() {
50 return Ok(());
51 }
52 if items.first().map(Value::is_object).unwrap_or(false) {
53 render_table(w, items)
54 } else {
55 for item in items {
56 writeln!(w, "{}", format_cell(item))?;
57 }
58 Ok(())
59 }
60 }
61 Value::Object(o) => {
62 if let Some(blocks) = crate::blocks::extract(v) {
64 return render_blocks(w, &blocks);
65 }
66 render_kv(w, o)
67 }
68 }
69}
70
71fn render_blocks(w: &mut dyn Write, blocks: &[crate::blocks::Block]) -> errors::Result<()> {
74 for b in blocks {
75 match b {
76 crate::blocks::Block::Text { text } => writeln!(w, "{text}")?,
77 crate::blocks::Block::Image { mime_type, data }
78 | crate::blocks::Block::Audio { mime_type, data } => {
79 let path = spill_to(&std::env::temp_dir(), mime_type, data)?;
80 writeln!(w, "{}", path.display())?;
81 }
82 }
83 }
84 Ok(())
85}
86
87pub fn spill_to(
89 dir: &std::path::Path,
90 mime_type: &str,
91 data: &str,
92) -> errors::Result<std::path::PathBuf> {
93 use std::sync::atomic::{AtomicU64, Ordering};
94 static SEQ: AtomicU64 = AtomicU64::new(0);
95 let bytes = crate::blocks::decode_base64(data)?;
96 let ext = crate::blocks::ext_for_mime(mime_type);
97 let name = format!(
98 "xyz-blk-{}-{}.{ext}",
99 std::process::id(),
100 SEQ.fetch_add(1, Ordering::Relaxed)
101 );
102 let path = dir.join(name);
103 std::fs::write(&path, bytes)?;
104 Ok(path)
105}
106
107fn fmt_number(n: &serde_json::Number) -> String {
108 if let Some(f) = n.as_f64()
110 && n.as_i64().is_none()
111 && n.as_u64().is_none()
112 {
113 return f.to_string();
114 }
115 n.to_string()
116}
117
118fn render_kv(w: &mut dyn Write, o: &serde_json::Map<String, Value>) -> errors::Result<()> {
119 if o.is_empty() {
120 return Ok(());
121 }
122 let width = o.keys().map(|k| k.len()).max().unwrap_or(0);
125 for (k, v) in o {
126 writeln!(w, "{k:<width$} {}", format_cell(v))?;
127 }
128 Ok(())
129}
130
131fn render_table(w: &mut dyn Write, items: &[Value]) -> errors::Result<()> {
132 let first = match items.first().and_then(Value::as_object) {
133 Some(o) => o,
134 None => return Ok(()),
135 };
136 let keys: Vec<&String> = first.keys().collect();
137 if keys.is_empty() {
138 return Ok(());
139 }
140 const MAX_PAD: usize = 200;
145 let cell_width = |c: &str| c.chars().count().min(MAX_PAD);
146 let mut widths: Vec<usize> = keys.iter().map(|k| cell_width(k)).collect();
147 let mut rows: Vec<Vec<String>> = Vec::with_capacity(items.len());
148 for item in items {
149 let mut row = Vec::with_capacity(keys.len());
150 match item.as_object() {
151 Some(o) => {
152 for (j, k) in keys.iter().enumerate() {
153 let cell = o.get(*k).map(format_cell).unwrap_or_default();
154 widths[j] = widths[j].max(cell_width(&cell));
155 row.push(cell);
156 }
157 }
158 None => {
159 for k in keys.iter() {
160 row.push(format_cell(item));
161 let _ = k;
162 }
163 }
164 }
165 rows.push(row);
166 }
167 write_row(
168 w,
169 &keys.iter().map(|k| k.to_string()).collect::<Vec<_>>(),
170 &widths,
171 )?;
172 let dashes: Vec<String> = widths.iter().map(|wd| "-".repeat(*wd)).collect();
173 write_row(w, &dashes, &widths)?;
174 for row in &rows {
175 write_row(w, row, &widths)?;
176 }
177 Ok(())
178}
179
180fn write_row(w: &mut dyn Write, cells: &[String], widths: &[usize]) -> errors::Result<()> {
181 for (i, c) in cells.iter().enumerate() {
182 if i > 0 {
183 write!(w, " ")?;
184 }
185 write!(w, "{c:<width$}", width = widths[i])?;
186 }
187 writeln!(w)?;
188 Ok(())
189}
190
191pub fn format_cell(v: &Value) -> String {
194 match v {
195 Value::Null => String::new(),
196 Value::String(s) => s.clone(),
197 Value::Bool(b) => b.to_string(),
198 Value::Number(n) => fmt_number(n),
199 Value::Array(items) => {
200 let inner = items.iter().map(format_cell).collect::<Vec<_>>().join(" ");
201 format!("[{inner}]")
202 }
203 Value::Object(o) => serde_json::to_string(o).unwrap_or_else(|_| "{}".to_string()),
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 fn render(v: &Value) -> String {
212 let mut out = Vec::new();
213 render_value(&mut out, v).unwrap();
214 String::from_utf8(out).unwrap()
215 }
216
217 #[test]
218 fn long_cells_do_not_panic_and_pad_is_capped() {
219 let long = "x".repeat(70_000);
221 let v = serde_json::json!([
222 {"a": long.clone(), "b": "y"},
223 {"a": "short", "b": "z"},
224 ]);
225 let text = render(&v);
226 assert!(text.contains(&long), "long cell content must be preserved");
227 let second = text.lines().nth(3).unwrap();
229 assert!(
230 second.len() < 1000,
231 "row padded to the long cell width: {} bytes",
232 second.len()
233 );
234 }
235
236 #[test]
237 fn cjk_cells_use_char_widths() {
238 let v = serde_json::json!([
239 {"name": "中文", "n": 1},
240 {"name": "ab", "n": 2},
241 ]);
242 let text = render(&v);
243 let rule = text.lines().nth(1).unwrap();
245 assert!(rule.starts_with("----"), "{rule}");
246 assert_eq!(rule.split(" ").next().unwrap().chars().count(), 4);
247 }
248}