Skip to main content

xyz_rust/cli/
render.rs

1// 结果渲染(Go cli.Render 的对应物):
2//
3//	null                 -> 什么都不输出
4//	string               -> 原始字符串
5//	bool / 数字          -> 裸值
6//	数组(对象元素)      -> 对齐表格(表头 + 分隔线 + 行)
7//	数组(标量元素)      -> 每行一个
8//	对象(struct 或 map) -> 对齐 "key  value" 列
9//
10// 结果从不包 {"data": ...} 信封:--json 直接输出裸 JSON 值而不是走
11// Render。与 Go 版的差异(已记录在 README 差异节):
12//   - Rust 侧渲染输入是序列化后的 serde_json::Value——struct 与
13//     同形 map 都表现为 Object,按 serde 产出顺序输出(Go 的 map 会
14//     按键排序;struct 保持声明序,preserve_order 已对齐后者);
15//   - 浮点裸值按 f64 Display 输出("3" 而非 "3.0"),对齐 Go 的 %v。
16
17use std::io::Write;
18
19use serde::Serialize;
20use serde_json::Value;
21
22use crate::errors;
23
24/// 把可序列化结果渲染成人类可读形态。
25pub 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
32/// 直接渲染一个 Value(Entry.invoke 的产物形态)。
33pub 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            // 保留块信封(spec §12.7):文本块内联、二进制块落盘打路径。
63            if let Some(blocks) = crate::blocks::extract(v) {
64                return render_blocks(w, &blocks);
65            }
66            render_kv(w, o)
67        }
68    }
69}
70
71/// 渲染块清单:text 直出;image/audio 落盘临时目录、打印文件路径——
72/// 大载荷不进终端(spec §12.7 的 CLI 投影)。
73fn 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
87/// 把一块 base64 载荷落盘:文件名 xyz-blk-<pid>-<seq>.<ext>。
88pub 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    // f64 走 Display:与 Go %v 一致的裸浮点形态("0.25"、"3")。
109    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    // 宽度按字节计(Go 的 len()),填充按字符计(Go 的 %-*s)——两者合一
123    // 才能与 Go 的 CJK 对齐逐字节一致。
124    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    // 宽度按字符计(fmt 的 `{:<width$}` 以字符补白),并对补白宽度封顶:
141    // Rust 的命名宽度参数是 u16(> u16::MAX 会 panic! "Formatting argument
142    // out of range"),超长单元格(引用方法列表等)曾直接打死渲染器;封顶
143    // 同时避免按全局最宽单元格把每行都补到几十 KB。
144    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
191/// 单元格/键值取值:指针解引用(JSON 里不存在指针对象)、切片括号
192/// 连接,其余按裸值。
193pub 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        // 70k chars > u16::MAX (the width argument limit) — used to panic.
220        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        // The padding width is capped: the short row must not carry 70k spaces.
228        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        // header rule width == char count of the widest cell (2), not bytes.
244        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}