1use std::collections::BTreeSet;
2
3use anyhow::Result;
4use clap::ValueEnum;
5use serde::Serialize;
6use serde_json::Value;
7use tabled::builder::Builder;
8use tabled::settings::Style;
9
10#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum, Default)]
11pub enum OutputFormat {
12 #[default]
13 Table,
14 Json,
15 Yaml,
16 Csv,
17 Quiet,
18}
19
20pub struct OutputRenderer {
21 format: OutputFormat,
22}
23
24impl OutputRenderer {
25 pub fn new(format: OutputFormat) -> Self {
26 Self { format }
27 }
28
29 pub fn format(&self) -> OutputFormat {
30 self.format
31 }
32
33 pub fn render<T: Serialize>(&self, value: &T) -> Result<()> {
34 let json_value = serde_json::to_value(value)?;
35
36 match self.format {
37 OutputFormat::Table => {
38 if !self.render_table(&json_value)? {
39 println!("{}", serde_json::to_string_pretty(&json_value)?);
40 }
41 }
42 OutputFormat::Json => {
43 println!("{}", serde_json::to_string_pretty(&json_value)?);
44 }
45 OutputFormat::Yaml => {
46 println!("{}", serde_yaml::to_string(&json_value)?);
47 }
48 OutputFormat::Csv => {
49 if !self.render_csv(&json_value)? {
50 println!("{}", serde_json::to_string_pretty(&json_value)?);
51 }
52 }
53 OutputFormat::Quiet => {
54 if !self.render_quiet(&json_value) {
55 println!("{}", serde_json::to_string_pretty(&json_value)?);
56 }
57 }
58 }
59
60 Ok(())
61 }
62
63 fn render_table(&self, value: &Value) -> Result<bool> {
64 let (headers, rows) = match Self::coerce_rows(value) {
65 Some(data) => data,
66 None => return Ok(false),
67 };
68
69 let mut builder = Builder::default();
70 builder.push_record(headers);
71 for row in rows {
72 builder.push_record(row);
73 }
74
75 let table = builder.build().with(Style::rounded()).to_string();
76 println!("{}", table);
77 Ok(true)
78 }
79
80 fn render_csv(&self, value: &Value) -> Result<bool> {
81 let (headers, rows) = match Self::coerce_rows(value) {
82 Some(data) => data,
83 None => return Ok(false),
84 };
85
86 println!("{}", headers.join(","));
87 for row in rows {
88 println!("{}", row.join(","));
89 }
90
91 Ok(true)
92 }
93
94 fn render_quiet(&self, value: &Value) -> bool {
95 match value {
96 Value::Array(rows) => {
97 let mut printed = false;
98 for row in rows {
99 if let Value::Object(obj) = row {
100 if let Some(id) = obj.get("id").and_then(Value::as_str) {
101 println!("{id}");
102 printed = true;
103 } else if let Some(key) = obj.keys().next() {
104 if let Some(val) = obj.get(key) {
105 println!("{}", val);
106 printed = true;
107 }
108 }
109 } else if !row.is_null() {
110 println!("{}", row);
111 printed = true;
112 }
113 }
114 printed
115 }
116 Value::Object(obj) => {
117 if let Some(id) = obj.get("id").and_then(Value::as_str) {
118 println!("{id}");
119 true
120 } else {
121 false
122 }
123 }
124 Value::Null => false,
125 other => {
126 println!("{}", other);
127 true
128 }
129 }
130 }
131
132 fn coerce_rows(value: &Value) -> Option<(Vec<String>, Vec<Vec<String>>)> {
133 let rows = match value {
134 Value::Array(rows) if !rows.is_empty() => rows,
135 _ => return None,
136 };
137
138 let mut headers = BTreeSet::new();
139 for row in rows {
140 if let Value::Object(obj) = row {
141 headers.extend(obj.keys().cloned());
142 }
143 }
144
145 if headers.is_empty() {
146 return None;
147 }
148
149 let headers_vec: Vec<String> = headers.into_iter().collect();
150 let mut data = Vec::with_capacity(rows.len());
151 for row in rows {
152 let mut record = Vec::with_capacity(headers_vec.len());
153 if let Value::Object(obj) = row {
154 for header in &headers_vec {
155 let cell = obj
156 .get(header)
157 .map(Self::value_to_string)
158 .unwrap_or_else(|| "".to_string());
159 record.push(cell);
160 }
161 }
162 data.push(record);
163 }
164
165 Some((headers_vec, data))
166 }
167
168 fn value_to_string(value: &Value) -> String {
169 match value {
170 Value::String(s) => s.clone(),
171 Value::Number(n) => n.to_string(),
172 Value::Bool(b) => b.to_string(),
173 Value::Null => String::new(),
174 other => serde_json::to_string(other).unwrap_or_default(),
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use serde_json::json;
183
184 #[test]
185 fn test_output_format_default() {
186 assert_eq!(OutputFormat::default(), OutputFormat::Table);
187 }
188
189 #[test]
190 fn test_renderer_new() {
191 let renderer = OutputRenderer::new(OutputFormat::Json);
192 assert_eq!(renderer.format(), OutputFormat::Json);
193 }
194
195 #[test]
196 fn test_coerce_rows_empty_array() {
197 let value = json!([]);
198 assert!(OutputRenderer::coerce_rows(&value).is_none());
199 }
200
201 #[test]
202 fn test_coerce_rows_single_object() {
203 let value = json!([
204 {"id": "1", "name": "Alice"},
205 {"id": "2", "name": "Bob"}
206 ]);
207
208 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
209 assert_eq!(headers.len(), 2);
210 assert!(headers.contains(&"id".to_string()));
211 assert!(headers.contains(&"name".to_string()));
212 assert_eq!(rows.len(), 2);
213 }
214
215 #[test]
216 fn test_coerce_rows_mixed_keys() {
217 let value = json!([
218 {"id": "1", "name": "Alice"},
219 {"id": "2", "email": "bob@example.com"}
220 ]);
221
222 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
223 assert_eq!(headers.len(), 3);
224 assert!(headers.contains(&"id".to_string()));
225 assert!(headers.contains(&"name".to_string()));
226 assert!(headers.contains(&"email".to_string()));
227
228 assert_eq!(
229 rows[0][headers.iter().position(|h| h == "id").unwrap()],
230 "1"
231 );
232 assert_eq!(
233 rows[0][headers.iter().position(|h| h == "name").unwrap()],
234 "Alice"
235 );
236 assert_eq!(
237 rows[0][headers.iter().position(|h| h == "email").unwrap()],
238 ""
239 );
240 }
241
242 #[test]
243 fn test_coerce_rows_not_array() {
244 let value = json!({"id": "1", "name": "Alice"});
245 assert!(OutputRenderer::coerce_rows(&value).is_none());
246 }
247
248 #[test]
249 fn test_coerce_rows_array_of_primitives() {
250 let value = json!(["one", "two", "three"]);
251 assert!(OutputRenderer::coerce_rows(&value).is_none());
252 }
253
254 #[test]
255 fn test_value_to_string_string() {
256 let value = json!("hello");
257 assert_eq!(OutputRenderer::value_to_string(&value), "hello");
258 }
259
260 #[test]
261 fn test_value_to_string_number() {
262 let value = json!(42);
263 assert_eq!(OutputRenderer::value_to_string(&value), "42");
264 }
265
266 #[test]
267 fn test_value_to_string_bool() {
268 let value = json!(true);
269 assert_eq!(OutputRenderer::value_to_string(&value), "true");
270 }
271
272 #[test]
273 fn test_value_to_string_null() {
274 let value = json!(null);
275 assert_eq!(OutputRenderer::value_to_string(&value), "");
276 }
277
278 #[test]
279 fn test_value_to_string_object() {
280 let value = json!({"key": "value"});
281 let result = OutputRenderer::value_to_string(&value);
282 assert!(result.contains("key"));
283 assert!(result.contains("value"));
284 }
285
286 #[test]
287 fn test_render_quiet_object_with_id() {
288 let value = json!({"id": "123", "name": "Test"});
289 let renderer = OutputRenderer::new(OutputFormat::Quiet);
290 assert!(renderer.render_quiet(&value));
291 }
292
293 #[test]
294 fn test_render_quiet_object_without_id() {
295 let value = json!({"name": "Test"});
296 let renderer = OutputRenderer::new(OutputFormat::Quiet);
297 assert!(!renderer.render_quiet(&value));
298 }
299
300 #[test]
301 fn test_render_quiet_array_with_ids() {
302 let value = json!([
303 {"id": "1", "name": "Alice"},
304 {"id": "2", "name": "Bob"}
305 ]);
306 let renderer = OutputRenderer::new(OutputFormat::Quiet);
307 assert!(renderer.render_quiet(&value));
308 }
309
310 #[test]
311 fn test_render_quiet_primitive() {
312 let value = json!("simple");
313 let renderer = OutputRenderer::new(OutputFormat::Quiet);
314 assert!(renderer.render_quiet(&value));
315 }
316
317 #[test]
318 fn test_render_quiet_null() {
319 let value = json!(null);
320 let renderer = OutputRenderer::new(OutputFormat::Quiet);
321 assert!(!renderer.render_quiet(&value));
322 }
323
324 #[test]
325 fn test_render_quiet_array_with_nulls() {
326 let value = json!([null, null]);
327 let renderer = OutputRenderer::new(OutputFormat::Quiet);
328 assert!(!renderer.render_quiet(&value));
329 }
330
331 #[derive(Serialize)]
332 struct TestStruct {
333 id: String,
334 name: String,
335 count: i32,
336 }
337
338 #[test]
339 fn test_render_json() {
340 let test_data = TestStruct {
341 id: "1".to_string(),
342 name: "Test".to_string(),
343 count: 42,
344 };
345
346 let renderer = OutputRenderer::new(OutputFormat::Json);
347 let result = renderer.render(&test_data);
348 assert!(result.is_ok());
349 }
350
351 #[test]
352 fn test_render_yaml() {
353 let test_data = TestStruct {
354 id: "1".to_string(),
355 name: "Test".to_string(),
356 count: 42,
357 };
358
359 let renderer = OutputRenderer::new(OutputFormat::Yaml);
360 let result = renderer.render(&test_data);
361 assert!(result.is_ok());
362 }
363
364 #[test]
365 fn test_render_table() {
366 let test_data = vec![
367 TestStruct {
368 id: "1".to_string(),
369 name: "Alice".to_string(),
370 count: 10,
371 },
372 TestStruct {
373 id: "2".to_string(),
374 name: "Bob".to_string(),
375 count: 20,
376 },
377 ];
378
379 let renderer = OutputRenderer::new(OutputFormat::Table);
380 let result = renderer.render(&test_data);
381 assert!(result.is_ok());
382 }
383
384 #[test]
385 fn test_render_csv() {
386 let test_data = vec![
387 TestStruct {
388 id: "1".to_string(),
389 name: "Alice".to_string(),
390 count: 10,
391 },
392 TestStruct {
393 id: "2".to_string(),
394 name: "Bob".to_string(),
395 count: 20,
396 },
397 ];
398
399 let renderer = OutputRenderer::new(OutputFormat::Csv);
400 let result = renderer.render(&test_data);
401 assert!(result.is_ok());
402 }
403}