1use std::io::Write;
8
9use serde_json::Value;
10
11use crate::errors;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Format {
16 #[default]
20 Auto,
21 Text,
23 Json,
25 JsonL,
27 Markdown,
29}
30
31impl Format {
32 pub fn parse(s: &str) -> Option<Format> {
34 match s {
35 "" | "auto" => Some(Format::Auto),
36 "text" => Some(Format::Text),
37 "json" => Some(Format::Json),
38 "jsonl" => Some(Format::JsonL),
39 "markdown" => Some(Format::Markdown),
40 _ => None,
41 }
42 }
43
44 pub fn as_str(&self) -> &'static str {
45 match self {
46 Format::Auto => "auto",
47 Format::Text => "text",
48 Format::Json => "json",
49 Format::JsonL => "jsonl",
50 Format::Markdown => "markdown",
51 }
52 }
53
54 pub fn is_machine(&self) -> bool {
57 matches!(self, Format::Json | Format::JsonL)
58 }
59
60 pub fn resolve(self, interactive: bool, ifmt: &str, pfmt: &str) -> Format {
63 if !matches!(self, Format::Auto) {
64 return self;
65 }
66 let pick = if interactive {
67 if ifmt.is_empty() { "text" } else { ifmt }
68 } else if pfmt.is_empty() {
69 "jsonl"
70 } else {
71 pfmt
72 };
73 Format::parse(pick).unwrap_or(if interactive {
74 Format::Text
75 } else {
76 Format::JsonL
77 })
78 }
79}
80
81pub fn render_jsonl(w: &mut dyn Write, v: &Value) -> errors::Result<()> {
83 match v {
84 Value::Null => Ok(()),
85 Value::Array(items) => {
86 for item in items {
87 writeln!(
88 w,
89 "{}",
90 serde_json::to_string(item).unwrap_or_else(|_| "null".into())
91 )?;
92 }
93 Ok(())
94 }
95 other => {
96 writeln!(
97 w,
98 "{}",
99 serde_json::to_string(other).unwrap_or_else(|_| "null".into())
100 )?;
101 Ok(())
102 }
103 }
104}
105
106pub fn render_markdown(w: &mut dyn Write, v: &Value) -> errors::Result<()> {
113 match v {
114 Value::Null => Ok(()),
115 Value::String(_) | Value::Bool(_) | Value::Number(_) => {
116 writeln!(w, "{}", md_escape(&cell(v)))?;
117 Ok(())
118 }
119 Value::Object(o) => md_kv(w, o),
120 Value::Array(items) => {
121 if items.is_empty() {
122 return Ok(());
123 }
124 if items[0].is_object() {
125 md_table(w, items)
126 } else {
127 for item in items {
128 writeln!(w, "- {}", md_escape(&cell(item)))?;
129 }
130 Ok(())
131 }
132 }
133 }
134}
135
136fn md_kv(w: &mut dyn Write, o: &serde_json::Map<String, Value>) -> errors::Result<()> {
137 if o.is_empty() {
138 return Ok(());
139 }
140 writeln!(w, "| Field | Value |")?;
141 writeln!(w, "| --- | --- |")?;
142 for (k, val) in o {
143 writeln!(w, "| {} | {} |", md_escape(k), md_escape(&cell(val)))?;
144 }
145 Ok(())
146}
147
148fn md_table(w: &mut dyn Write, items: &[Value]) -> errors::Result<()> {
149 let Some(first) = items[0].as_object() else {
150 return Ok(());
151 };
152 let keys: Vec<&String> = first.keys().collect();
153 if keys.is_empty() {
154 return Ok(());
155 }
156 write!(w, "|")?;
157 for k in &keys {
158 write!(w, " {} |", md_escape(k))?;
159 }
160 writeln!(w)?;
161 write!(w, "|")?;
162 for _ in &keys {
163 write!(w, " --- |")?;
164 }
165 writeln!(w)?;
166 for item in items {
167 write!(w, "|")?;
168 let row = item.as_object();
169 for k in &keys {
170 let val = row.and_then(|r| r.get(k.as_str()));
171 let text = match val {
172 Some(v) => md_escape(&cell(v)),
173 None => String::new(),
174 };
175 write!(w, " {text} |")?;
176 }
177 writeln!(w)?;
178 }
179 Ok(())
180}
181
182fn cell(v: &Value) -> String {
184 crate::cli::render::format_cell(v)
185}
186
187fn md_escape(s: &str) -> String {
189 s.replace("\r\n", "<br>")
190 .replace('\n', "<br>")
191 .replace('|', "\\|")
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use serde_json::json;
198
199 fn md(v: &Value) -> String {
200 let mut buf = Vec::new();
201 render_markdown(&mut buf, v).unwrap();
202 String::from_utf8(buf).unwrap()
203 }
204
205 fn jsonl(v: &Value) -> String {
206 let mut buf = Vec::new();
207 render_jsonl(&mut buf, v).unwrap();
208 String::from_utf8(buf).unwrap()
209 }
210
211 #[test]
212 fn jsonl_shapes() {
213 assert_eq!(jsonl(&json!(null)), "");
214 assert_eq!(jsonl(&json!([1, 2])), "1\n2\n");
215 assert_eq!(jsonl(&json!({"a": 1})), "{\"a\":1}\n");
216 assert_eq!(jsonl(&json!("x")), "\"x\"\n");
217 }
218
219 #[test]
220 fn markdown_shapes() {
221 assert_eq!(md(&json!(null)), "");
222 assert_eq!(md(&json!("hi")), "hi\n");
223 assert_eq!(
224 md(&json!({"k": "v", "long": 1})),
225 "| Field | Value |\n| --- | --- |\n| k | v |\n| long | 1 |\n"
226 );
227 assert_eq!(md(&json!([1, "a"])), "- 1\n- a\n");
228 assert_eq!(
229 md(&json!([{"a": 1, "b": 2}, {"a": 3, "b": 4}])),
230 "| a | b |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |\n"
231 );
232 assert_eq!(
234 md(&json!({"k": "a|b\nc"})),
235 "| Field | Value |\n| --- | --- |\n| k | a\\|b<br>c |\n"
236 );
237 assert_eq!(md(&json!({})), "");
239 assert_eq!(md(&json!([])), "");
240 }
241
242 #[test]
243 fn format_parsing() {
244 assert_eq!(Format::parse(""), Some(Format::Auto));
245 assert_eq!(Format::parse("auto"), Some(Format::Auto));
246 assert_eq!(Format::parse("jsonl"), Some(Format::JsonL));
247 assert_eq!(Format::parse("MARKDOWN"), None);
248 assert!(Format::Json.is_machine() && !Format::Markdown.is_machine());
249 }
250}