1use std::fmt::Write as _;
7
8use clap::ValueEnum;
9use serde_json::Value;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
12pub enum Format {
13 #[default]
14 Human,
15 Tsv,
16 Json,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub struct FormatChoice {
21 pub json: bool,
22 pub tsv: bool,
23}
24
25impl FormatChoice {
26 pub fn resolve(self) -> Format {
27 if self.json {
28 Format::Json
29 } else if self.tsv {
30 Format::Tsv
31 } else {
32 Format::Human
33 }
34 }
35}
36
37pub fn render_table(headers: &[&str], rows: &[Vec<String>], fmt: Format) -> String {
40 if rows.is_empty() && matches!(fmt, Format::Human) {
41 return String::from("(no rows)\n");
42 }
43 match fmt {
44 Format::Tsv => {
45 rows.iter()
46 .map(|r| r.join("\t"))
47 .collect::<Vec<_>>()
48 .join("\n")
49 .trim_end()
50 .to_string()
51 + "\n"
52 }
53 Format::Json => {
54 let arr: Vec<Value> = rows
55 .iter()
56 .map(|r| {
57 let mut m = serde_json::Map::new();
58 for (i, h) in headers.iter().enumerate() {
59 m.insert((*h).to_string(), Value::String(r[i].clone()));
60 }
61 Value::Object(m)
62 })
63 .collect();
64 serde_json::to_string_pretty(&arr).unwrap_or_default() + "\n"
65 }
66 Format::Human => {
67 let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
68 for row in rows {
69 for (i, cell) in row.iter().enumerate() {
70 if cell.len() > widths[i] {
71 widths[i] = cell.len();
72 }
73 }
74 }
75 let mut out = String::new();
76 for (i, h) in headers.iter().enumerate() {
77 let _ = write!(out, "{:<width$}", h, width = widths[i]);
78 if i + 1 < headers.len() {
79 out.push_str(" ");
80 }
81 }
82 out.push('\n');
83 for row in rows {
84 for (i, cell) in row.iter().enumerate() {
85 let _ = write!(out, "{:<width$}", cell, width = widths[i]);
86 if i + 1 < row.len() {
87 out.push_str(" ");
88 }
89 }
90 out.push('\n');
91 }
92 out
93 }
94 }
95}
96
97pub fn render_record(pairs: &[(&str, String)], fmt: Format) -> String {
99 match fmt {
100 Format::Json => {
101 let mut m = serde_json::Map::new();
102 for (k, v) in pairs {
103 m.insert((*k).to_string(), Value::String(v.clone()));
104 }
105 serde_json::to_string_pretty(&Value::Object(m)).unwrap_or_default() + "\n"
106 }
107 Format::Tsv => {
108 pairs
109 .iter()
110 .map(|(_, v)| v.as_str())
111 .collect::<Vec<_>>()
112 .join("\t")
113 + "\n"
114 }
115 Format::Human => {
116 let key_w = pairs.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
117 let mut out = String::new();
118 for (k, v) in pairs {
119 let _ = writeln!(out, "{:<key_w$} {}", k, v, key_w = key_w);
120 }
121 out
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn human_table_pads_columns() {
132 let s = render_table(
133 &["a", "bbb"],
134 &[
135 vec!["1".into(), "two".into()],
136 vec!["12".into(), "x".into()],
137 ],
138 Format::Human,
139 );
140 let lines: Vec<&str> = s.lines().collect();
141 assert_eq!(lines[0], "a bbb");
142 assert_eq!(lines[1], "1 two");
143 assert_eq!(lines[2], "12 x ");
144 }
145
146 #[test]
147 fn tsv_table_uses_tabs() {
148 let s = render_table(&["a", "b"], &[vec!["1".into(), "2".into()]], Format::Tsv);
149 assert_eq!(s, "1\t2\n");
150 }
151
152 #[test]
153 fn json_table_is_array_of_objects() {
154 let s = render_table(&["a", "b"], &[vec!["1".into(), "2".into()]], Format::Json);
155 let v: Value = serde_json::from_str(s.trim()).unwrap();
156 assert_eq!(v[0]["a"], "1");
157 assert_eq!(v[0]["b"], "2");
158 }
159
160 #[test]
161 fn empty_human_table_is_marker() {
162 let s = render_table(&["a"], &[], Format::Human);
163 assert_eq!(s, "(no rows)\n");
164 }
165
166 #[test]
167 fn format_choice_priority() {
168 assert_eq!(
169 FormatChoice {
170 json: true,
171 tsv: true
172 }
173 .resolve(),
174 Format::Json
175 );
176 assert_eq!(
177 FormatChoice {
178 json: false,
179 tsv: true
180 }
181 .resolve(),
182 Format::Tsv
183 );
184 assert_eq!(FormatChoice::default().resolve(), Format::Human);
185 }
186}