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
10pub mod colors;
11
12pub use colors::StatusFormatter;
13
14#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum, Default)]
15pub enum OutputFormat {
16 #[default]
17 Table,
18 Json,
19 Yaml,
20 Csv,
21 Quiet,
22 Markdown,
23}
24
25pub struct OutputRenderer {
26 format: OutputFormat,
27}
28
29impl OutputRenderer {
30 pub fn new(format: OutputFormat) -> Self {
31 Self { format }
32 }
33
34 pub fn format(&self) -> OutputFormat {
35 self.format
36 }
37
38 pub fn render<T: Serialize>(&self, value: &T) -> Result<()> {
39 let json_value = serde_json::to_value(value)?;
40
41 match self.format {
42 OutputFormat::Table => {
43 if !self.render_table(&json_value)? {
44 println!("{}", serde_json::to_string_pretty(&json_value)?);
45 }
46 }
47 OutputFormat::Json => {
48 println!("{}", serde_json::to_string_pretty(&json_value)?);
49 }
50 OutputFormat::Yaml => {
51 println!("{}", serde_yaml::to_string(&json_value)?);
52 }
53 OutputFormat::Csv => {
54 if !self.render_csv(&json_value)? {
55 println!("{}", serde_json::to_string_pretty(&json_value)?);
56 }
57 }
58 OutputFormat::Quiet => {
59 if !self.render_quiet(&json_value) {
60 println!("{}", serde_json::to_string_pretty(&json_value)?);
61 }
62 }
63 OutputFormat::Markdown => {
64 if !self.render_markdown_table(&json_value)? {
65 self.render_markdown_single(&json_value)?;
66 }
67 }
68 }
69
70 Ok(())
71 }
72
73 fn render_table(&self, value: &Value) -> Result<bool> {
74 let (headers, rows) = match Self::coerce_rows(value) {
75 Some(data) => data,
76 None => return Ok(false),
77 };
78
79 let mut builder = Builder::default();
80 builder.push_record(headers);
81 for row in rows {
82 builder.push_record(row);
83 }
84
85 let table = builder.build().with(Style::rounded()).to_string();
86 println!("{}", table);
87 Ok(true)
88 }
89
90 fn render_csv(&self, value: &Value) -> Result<bool> {
91 let (headers, rows) = match Self::coerce_rows(value) {
92 Some(data) => data,
93 None => return Ok(false),
94 };
95
96 println!("{}", headers.join(","));
97 for row in rows {
98 println!("{}", row.join(","));
99 }
100
101 Ok(true)
102 }
103
104 fn render_quiet(&self, value: &Value) -> bool {
105 match value {
106 Value::Array(rows) => {
107 let mut printed = false;
108 for row in rows {
109 if let Value::Object(obj) = row {
110 if let Some(id) = obj.get("id").and_then(Value::as_str) {
111 println!("{id}");
112 printed = true;
113 } else if let Some(key) = obj.keys().next() {
114 if let Some(val) = obj.get(key) {
115 println!("{}", val);
116 printed = true;
117 }
118 }
119 } else if !row.is_null() {
120 println!("{}", row);
121 printed = true;
122 }
123 }
124 printed
125 }
126 Value::Object(obj) => {
127 if let Some(id) = obj.get("id").and_then(Value::as_str) {
128 println!("{id}");
129 true
130 } else {
131 false
132 }
133 }
134 Value::Null => false,
135 other => {
136 println!("{}", other);
137 true
138 }
139 }
140 }
141
142 pub fn render_raw(&self, content: &str) -> Result<()> {
144 println!("{content}");
145 Ok(())
146 }
147
148 fn render_markdown_table(&self, value: &Value) -> Result<bool> {
149 let (headers, rows) = match Self::coerce_rows(value) {
150 Some(data) => data,
151 None => return Ok(false),
152 };
153
154 let header_line: String = headers
156 .iter()
157 .map(|h| h.replace('|', "\\|"))
158 .collect::<Vec<_>>()
159 .join(" | ");
160 println!("| {} |", header_line);
161
162 let separator: String = headers
164 .iter()
165 .map(|_| "---")
166 .collect::<Vec<_>>()
167 .join(" | ");
168 println!("| {} |", separator);
169
170 for row in rows {
172 let cells: String = row
173 .iter()
174 .map(|c| c.replace('|', "\\|"))
175 .collect::<Vec<_>>()
176 .join(" | ");
177 println!("| {} |", cells);
178 }
179
180 Ok(true)
181 }
182
183 fn render_markdown_single(&self, value: &Value) -> Result<bool> {
184 if let Value::Object(obj) = value {
185 for (key, val) in obj {
186 let display = Self::value_to_string(val);
187 println!("**{}**: {}", key, display);
188 }
189 Ok(true)
190 } else {
191 println!("{}", serde_json::to_string_pretty(value)?);
192 Ok(true)
193 }
194 }
195
196 fn coerce_rows(value: &Value) -> Option<(Vec<String>, Vec<Vec<String>>)> {
197 let rows = match value {
198 Value::Array(rows) if !rows.is_empty() => rows,
199 _ => return None,
200 };
201
202 let mut headers = BTreeSet::new();
203 for row in rows {
204 if let Value::Object(obj) = row {
205 headers.extend(obj.keys().cloned());
206 }
207 }
208
209 if headers.is_empty() {
210 return None;
211 }
212
213 let headers_vec: Vec<String> = headers.into_iter().collect();
214 let mut data = Vec::with_capacity(rows.len());
215 for row in rows {
216 let mut record = Vec::with_capacity(headers_vec.len());
217 if let Value::Object(obj) = row {
218 for header in &headers_vec {
219 let cell = obj
220 .get(header)
221 .map(Self::value_to_string)
222 .unwrap_or_else(|| "".to_string());
223 record.push(cell);
224 }
225 }
226 data.push(record);
227 }
228
229 Some((headers_vec, data))
230 }
231
232 fn value_to_string(value: &Value) -> String {
233 match value {
234 Value::String(s) => s.clone(),
235 Value::Number(n) => n.to_string(),
236 Value::Bool(b) => b.to_string(),
237 Value::Null => String::new(),
238 other => serde_json::to_string(other).unwrap_or_default(),
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use serde_json::json;
247
248 #[test]
249 fn test_output_format_default() {
250 assert_eq!(OutputFormat::default(), OutputFormat::Table);
251 }
252
253 #[test]
254 fn test_renderer_new() {
255 let renderer = OutputRenderer::new(OutputFormat::Json);
256 assert_eq!(renderer.format(), OutputFormat::Json);
257 }
258
259 #[test]
260 fn test_coerce_rows_empty_array() {
261 let value = json!([]);
262 assert!(OutputRenderer::coerce_rows(&value).is_none());
263 }
264
265 #[test]
266 fn test_coerce_rows_single_object() {
267 let value = json!([
268 {"id": "1", "name": "Alice"},
269 {"id": "2", "name": "Bob"}
270 ]);
271
272 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
273 assert_eq!(headers.len(), 2);
274 assert!(headers.contains(&"id".to_string()));
275 assert!(headers.contains(&"name".to_string()));
276 assert_eq!(rows.len(), 2);
277 }
278
279 #[test]
280 fn test_coerce_rows_mixed_keys() {
281 let value = json!([
282 {"id": "1", "name": "Alice"},
283 {"id": "2", "email": "bob@example.com"}
284 ]);
285
286 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
287 assert_eq!(headers.len(), 3);
288 assert!(headers.contains(&"id".to_string()));
289 assert!(headers.contains(&"name".to_string()));
290 assert!(headers.contains(&"email".to_string()));
291
292 assert_eq!(
293 rows[0][headers.iter().position(|h| h == "id").unwrap()],
294 "1"
295 );
296 assert_eq!(
297 rows[0][headers.iter().position(|h| h == "name").unwrap()],
298 "Alice"
299 );
300 assert_eq!(
301 rows[0][headers.iter().position(|h| h == "email").unwrap()],
302 ""
303 );
304 }
305
306 #[test]
307 fn test_coerce_rows_not_array() {
308 let value = json!({"id": "1", "name": "Alice"});
309 assert!(OutputRenderer::coerce_rows(&value).is_none());
310 }
311
312 #[test]
313 fn test_coerce_rows_array_of_primitives() {
314 let value = json!(["one", "two", "three"]);
315 assert!(OutputRenderer::coerce_rows(&value).is_none());
316 }
317
318 #[test]
319 fn test_value_to_string_string() {
320 let value = json!("hello");
321 assert_eq!(OutputRenderer::value_to_string(&value), "hello");
322 }
323
324 #[test]
325 fn test_value_to_string_number() {
326 let value = json!(42);
327 assert_eq!(OutputRenderer::value_to_string(&value), "42");
328 }
329
330 #[test]
331 fn test_value_to_string_bool() {
332 let value = json!(true);
333 assert_eq!(OutputRenderer::value_to_string(&value), "true");
334 }
335
336 #[test]
337 fn test_value_to_string_null() {
338 let value = json!(null);
339 assert_eq!(OutputRenderer::value_to_string(&value), "");
340 }
341
342 #[test]
343 fn test_value_to_string_object() {
344 let value = json!({"key": "value"});
345 let result = OutputRenderer::value_to_string(&value);
346 assert!(result.contains("key"));
347 assert!(result.contains("value"));
348 }
349
350 #[test]
351 fn test_render_quiet_object_with_id() {
352 let value = json!({"id": "123", "name": "Test"});
353 let renderer = OutputRenderer::new(OutputFormat::Quiet);
354 assert!(renderer.render_quiet(&value));
355 }
356
357 #[test]
358 fn test_render_quiet_object_without_id() {
359 let value = json!({"name": "Test"});
360 let renderer = OutputRenderer::new(OutputFormat::Quiet);
361 assert!(!renderer.render_quiet(&value));
362 }
363
364 #[test]
365 fn test_render_quiet_array_with_ids() {
366 let value = json!([
367 {"id": "1", "name": "Alice"},
368 {"id": "2", "name": "Bob"}
369 ]);
370 let renderer = OutputRenderer::new(OutputFormat::Quiet);
371 assert!(renderer.render_quiet(&value));
372 }
373
374 #[test]
375 fn test_render_quiet_primitive() {
376 let value = json!("simple");
377 let renderer = OutputRenderer::new(OutputFormat::Quiet);
378 assert!(renderer.render_quiet(&value));
379 }
380
381 #[test]
382 fn test_render_quiet_null() {
383 let value = json!(null);
384 let renderer = OutputRenderer::new(OutputFormat::Quiet);
385 assert!(!renderer.render_quiet(&value));
386 }
387
388 #[test]
389 fn test_render_quiet_array_with_nulls() {
390 let value = json!([null, null]);
391 let renderer = OutputRenderer::new(OutputFormat::Quiet);
392 assert!(!renderer.render_quiet(&value));
393 }
394
395 #[derive(Serialize)]
396 struct TestStruct {
397 id: String,
398 name: String,
399 count: i32,
400 }
401
402 #[test]
403 fn test_render_json() {
404 let test_data = TestStruct {
405 id: "1".to_string(),
406 name: "Test".to_string(),
407 count: 42,
408 };
409
410 let renderer = OutputRenderer::new(OutputFormat::Json);
411 let result = renderer.render(&test_data);
412 assert!(result.is_ok());
413 }
414
415 #[test]
416 fn test_render_yaml() {
417 let test_data = TestStruct {
418 id: "1".to_string(),
419 name: "Test".to_string(),
420 count: 42,
421 };
422
423 let renderer = OutputRenderer::new(OutputFormat::Yaml);
424 let result = renderer.render(&test_data);
425 assert!(result.is_ok());
426 }
427
428 #[test]
429 fn test_render_table() {
430 let test_data = vec![
431 TestStruct {
432 id: "1".to_string(),
433 name: "Alice".to_string(),
434 count: 10,
435 },
436 TestStruct {
437 id: "2".to_string(),
438 name: "Bob".to_string(),
439 count: 20,
440 },
441 ];
442
443 let renderer = OutputRenderer::new(OutputFormat::Table);
444 let result = renderer.render(&test_data);
445 assert!(result.is_ok());
446 }
447
448 #[test]
449 fn test_render_csv() {
450 let test_data = vec![
451 TestStruct {
452 id: "1".to_string(),
453 name: "Alice".to_string(),
454 count: 10,
455 },
456 TestStruct {
457 id: "2".to_string(),
458 name: "Bob".to_string(),
459 count: 20,
460 },
461 ];
462
463 let renderer = OutputRenderer::new(OutputFormat::Csv);
464 let result = renderer.render(&test_data);
465 assert!(result.is_ok());
466 }
467
468 #[test]
469 fn test_render_markdown_table() {
470 let test_data = vec![
471 TestStruct {
472 id: "1".to_string(),
473 name: "Alice".to_string(),
474 count: 10,
475 },
476 TestStruct {
477 id: "2".to_string(),
478 name: "Bob".to_string(),
479 count: 20,
480 },
481 ];
482
483 let renderer = OutputRenderer::new(OutputFormat::Markdown);
484 let result = renderer.render(&test_data);
485 assert!(result.is_ok());
486 }
487
488 #[test]
489 fn test_render_markdown_single_object() {
490 let test_data = TestStruct {
491 id: "1".to_string(),
492 name: "Test".to_string(),
493 count: 42,
494 };
495
496 let renderer = OutputRenderer::new(OutputFormat::Markdown);
497 let result = renderer.render(&test_data);
498 assert!(result.is_ok());
499 }
500
501 #[test]
502 fn test_render_markdown_pipe_escaping() {
503 let value = json!([
504 {"col": "a|b", "val": "x|y"}
505 ]);
506 let renderer = OutputRenderer::new(OutputFormat::Markdown);
507 assert!(renderer.render_markdown_table(&value).unwrap());
509 }
510
511 #[test]
512 fn test_render_raw() {
513 let renderer = OutputRenderer::new(OutputFormat::Markdown);
514 let result = renderer.render_raw("# Hello\n\nWorld");
515 assert!(result.is_ok());
516 }
517}