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 envelope: bool,
28}
29
30#[derive(Serialize)]
32struct ListEnvelope<'a, T: Serialize> {
33 data: &'a [T],
34 count: usize,
35}
36
37impl OutputRenderer {
38 pub fn new(format: OutputFormat) -> Self {
39 Self {
40 format,
41 envelope: false,
42 }
43 }
44
45 pub fn with_envelope(mut self, envelope: bool) -> Self {
46 self.envelope = envelope;
47 self
48 }
49
50 pub fn format(&self) -> OutputFormat {
51 self.format
52 }
53
54 pub fn render<T: Serialize>(&self, value: &T) -> Result<()> {
55 let json_value = serde_json::to_value(value)?;
56
57 match self.format {
58 OutputFormat::Table => {
59 if !self.render_table(&json_value)? {
60 println!("{}", serde_json::to_string_pretty(&json_value)?);
61 }
62 }
63 OutputFormat::Json => {
64 println!("{}", serde_json::to_string_pretty(&json_value)?);
65 }
66 OutputFormat::Yaml => {
67 println!("{}", serde_yaml::to_string(&json_value)?);
68 }
69 OutputFormat::Csv => {
70 if !self.render_csv(&json_value)? {
71 println!("{}", serde_json::to_string_pretty(&json_value)?);
72 }
73 }
74 OutputFormat::Quiet => {
75 if !self.render_quiet(&json_value) {
76 println!("{}", serde_json::to_string_pretty(&json_value)?);
77 }
78 }
79 OutputFormat::Markdown => {
80 if !self.render_markdown_table(&json_value)? {
81 self.render_markdown_single(&json_value)?;
82 }
83 }
84 }
85
86 Ok(())
87 }
88
89 pub fn render_list<T: Serialize>(&self, items: &[T]) -> Result<()> {
92 if self.envelope {
93 match self.format {
94 OutputFormat::Json => {
95 let envelope = ListEnvelope {
96 data: items,
97 count: items.len(),
98 };
99 println!("{}", serde_json::to_string_pretty(&envelope)?);
100 return Ok(());
101 }
102 OutputFormat::Yaml => {
103 let envelope = ListEnvelope {
104 data: items,
105 count: items.len(),
106 };
107 println!("{}", serde_yaml::to_string(&envelope)?);
108 return Ok(());
109 }
110 _ => {}
111 }
112 }
113 if items.is_empty() {
114 match self.format {
115 OutputFormat::Quiet | OutputFormat::Csv => return Ok(()),
120 _ => {}
121 }
122 }
123 self.render(&items)
124 }
125
126 pub fn render_list_or_empty<T: Serialize>(
133 &self,
134 items: &[T],
135 empty_message: &str,
136 ) -> Result<()> {
137 if items.is_empty() && matches!(self.format, OutputFormat::Table | OutputFormat::Markdown) {
141 println!("{empty_message}");
142 return Ok(());
143 }
144 self.render_list(items)
145 }
146
147 fn render_table(&self, value: &Value) -> Result<bool> {
148 let (headers, rows) = match Self::coerce_rows(value) {
149 Some(data) => data,
150 None => return Ok(false),
151 };
152
153 let mut builder = Builder::default();
154 builder.push_record(headers);
155 for row in rows {
156 builder.push_record(row);
157 }
158
159 let table = builder.build().with(Style::rounded()).to_string();
160 println!("{}", table);
161 Ok(true)
162 }
163
164 fn render_csv(&self, value: &Value) -> Result<bool> {
165 let (headers, rows) = match Self::coerce_rows(value) {
166 Some(data) => data,
167 None => return Ok(false),
168 };
169
170 println!("{}", Self::csv_record(&headers));
171 for row in rows {
172 println!("{}", Self::csv_record(&row));
173 }
174
175 Ok(true)
176 }
177
178 fn csv_record(fields: &[String]) -> String {
185 fields
186 .iter()
187 .map(|f| Self::csv_field(f))
188 .collect::<Vec<_>>()
189 .join(",")
190 }
191
192 fn csv_field(field: &str) -> String {
193 if field.contains([',', '"', '\n', '\r']) {
194 format!("\"{}\"", field.replace('"', "\"\""))
195 } else {
196 field.to_string()
197 }
198 }
199
200 fn render_quiet(&self, value: &Value) -> bool {
201 match value {
202 Value::Array(rows) => {
203 let mut printed = false;
204 for row in rows {
205 if let Value::Object(obj) = row {
206 if let Some(id) = obj.get("id").and_then(Value::as_str) {
207 println!("{id}");
208 printed = true;
209 } else if let Some(key) = obj.keys().next() {
210 if let Some(val) = obj.get(key) {
211 println!("{}", val);
212 printed = true;
213 }
214 }
215 } else if !row.is_null() {
216 println!("{}", row);
217 printed = true;
218 }
219 }
220 printed
221 }
222 Value::Object(obj) => {
223 if let Some(id) = obj.get("id").and_then(Value::as_str) {
224 println!("{id}");
225 true
226 } else {
227 false
228 }
229 }
230 Value::Null => false,
231 other => {
232 println!("{}", other);
233 true
234 }
235 }
236 }
237
238 pub fn render_raw(&self, content: &str) -> Result<()> {
240 println!("{content}");
241 Ok(())
242 }
243
244 fn render_markdown_table(&self, value: &Value) -> Result<bool> {
245 let (headers, rows) = match Self::coerce_rows(value) {
246 Some(data) => data,
247 None => return Ok(false),
248 };
249
250 let header_line: String = headers
252 .iter()
253 .map(|h| Self::markdown_cell(h))
254 .collect::<Vec<_>>()
255 .join(" | ");
256 println!("| {} |", header_line);
257
258 let separator: String = headers
260 .iter()
261 .map(|_| "---")
262 .collect::<Vec<_>>()
263 .join(" | ");
264 println!("| {} |", separator);
265
266 for row in rows {
268 let cells: String = row
269 .iter()
270 .map(|c| Self::markdown_cell(c))
271 .collect::<Vec<_>>()
272 .join(" | ");
273 println!("| {} |", cells);
274 }
275
276 Ok(true)
277 }
278
279 fn markdown_cell(cell: &str) -> String {
285 cell.replace('|', "\\|")
286 .replace("\r\n", "<br>")
287 .replace(['\n', '\r'], "<br>")
288 }
289
290 fn render_markdown_single(&self, value: &Value) -> Result<bool> {
291 if let Value::Object(obj) = value {
292 for (key, val) in obj {
293 let display = Self::value_to_string(val);
294 println!("**{}**: {}", key, display);
295 }
296 Ok(true)
297 } else {
298 println!("{}", serde_json::to_string_pretty(value)?);
299 Ok(true)
300 }
301 }
302
303 fn coerce_rows(value: &Value) -> Option<(Vec<String>, Vec<Vec<String>>)> {
304 let rows = match value {
305 Value::Array(rows) if !rows.is_empty() => rows,
306 _ => return None,
307 };
308
309 let mut headers = BTreeSet::new();
310 for row in rows {
311 if let Value::Object(obj) = row {
312 headers.extend(obj.keys().cloned());
313 }
314 }
315
316 if headers.is_empty() {
317 return None;
318 }
319
320 let headers_vec: Vec<String> = headers.into_iter().collect();
321 let mut data = Vec::with_capacity(rows.len());
322 for row in rows {
323 let mut record = Vec::with_capacity(headers_vec.len());
324 if let Value::Object(obj) = row {
325 for header in &headers_vec {
326 let cell = obj
327 .get(header)
328 .map(Self::value_to_string)
329 .unwrap_or_else(|| "".to_string());
330 record.push(cell);
331 }
332 }
333 data.push(record);
334 }
335
336 Some((headers_vec, data))
337 }
338
339 fn value_to_string(value: &Value) -> String {
340 match value {
341 Value::String(s) => s.clone(),
342 Value::Number(n) => n.to_string(),
343 Value::Bool(b) => b.to_string(),
344 Value::Null => String::new(),
345 other => serde_json::to_string(other).unwrap_or_default(),
346 }
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use serde_json::json;
354
355 #[test]
356 fn test_output_format_default() {
357 assert_eq!(OutputFormat::default(), OutputFormat::Table);
358 }
359
360 #[test]
361 fn test_renderer_new() {
362 let renderer = OutputRenderer::new(OutputFormat::Json);
363 assert_eq!(renderer.format(), OutputFormat::Json);
364 }
365
366 #[test]
367 fn test_coerce_rows_empty_array() {
368 let value = json!([]);
369 assert!(OutputRenderer::coerce_rows(&value).is_none());
370 }
371
372 #[test]
373 fn test_coerce_rows_single_object() {
374 let value = json!([
375 {"id": "1", "name": "Alice"},
376 {"id": "2", "name": "Bob"}
377 ]);
378
379 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
380 assert_eq!(headers.len(), 2);
381 assert!(headers.contains(&"id".to_string()));
382 assert!(headers.contains(&"name".to_string()));
383 assert_eq!(rows.len(), 2);
384 }
385
386 #[test]
387 fn test_coerce_rows_mixed_keys() {
388 let value = json!([
389 {"id": "1", "name": "Alice"},
390 {"id": "2", "email": "bob@example.com"}
391 ]);
392
393 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
394 assert_eq!(headers.len(), 3);
395 assert!(headers.contains(&"id".to_string()));
396 assert!(headers.contains(&"name".to_string()));
397 assert!(headers.contains(&"email".to_string()));
398
399 assert_eq!(
400 rows[0][headers.iter().position(|h| h == "id").unwrap()],
401 "1"
402 );
403 assert_eq!(
404 rows[0][headers.iter().position(|h| h == "name").unwrap()],
405 "Alice"
406 );
407 assert_eq!(
408 rows[0][headers.iter().position(|h| h == "email").unwrap()],
409 ""
410 );
411 }
412
413 #[test]
414 fn test_coerce_rows_not_array() {
415 let value = json!({"id": "1", "name": "Alice"});
416 assert!(OutputRenderer::coerce_rows(&value).is_none());
417 }
418
419 #[test]
420 fn test_coerce_rows_array_of_primitives() {
421 let value = json!(["one", "two", "three"]);
422 assert!(OutputRenderer::coerce_rows(&value).is_none());
423 }
424
425 #[test]
426 fn test_value_to_string_string() {
427 let value = json!("hello");
428 assert_eq!(OutputRenderer::value_to_string(&value), "hello");
429 }
430
431 #[test]
432 fn test_value_to_string_number() {
433 let value = json!(42);
434 assert_eq!(OutputRenderer::value_to_string(&value), "42");
435 }
436
437 #[test]
438 fn test_value_to_string_bool() {
439 let value = json!(true);
440 assert_eq!(OutputRenderer::value_to_string(&value), "true");
441 }
442
443 #[test]
444 fn test_value_to_string_null() {
445 let value = json!(null);
446 assert_eq!(OutputRenderer::value_to_string(&value), "");
447 }
448
449 #[test]
450 fn test_value_to_string_object() {
451 let value = json!({"key": "value"});
452 let result = OutputRenderer::value_to_string(&value);
453 assert!(result.contains("key"));
454 assert!(result.contains("value"));
455 }
456
457 #[test]
458 fn test_render_quiet_object_with_id() {
459 let value = json!({"id": "123", "name": "Test"});
460 let renderer = OutputRenderer::new(OutputFormat::Quiet);
461 assert!(renderer.render_quiet(&value));
462 }
463
464 #[test]
465 fn test_render_quiet_object_without_id() {
466 let value = json!({"name": "Test"});
467 let renderer = OutputRenderer::new(OutputFormat::Quiet);
468 assert!(!renderer.render_quiet(&value));
469 }
470
471 #[test]
472 fn test_render_quiet_array_with_ids() {
473 let value = json!([
474 {"id": "1", "name": "Alice"},
475 {"id": "2", "name": "Bob"}
476 ]);
477 let renderer = OutputRenderer::new(OutputFormat::Quiet);
478 assert!(renderer.render_quiet(&value));
479 }
480
481 #[test]
482 fn test_render_quiet_primitive() {
483 let value = json!("simple");
484 let renderer = OutputRenderer::new(OutputFormat::Quiet);
485 assert!(renderer.render_quiet(&value));
486 }
487
488 #[test]
489 fn test_render_quiet_null() {
490 let value = json!(null);
491 let renderer = OutputRenderer::new(OutputFormat::Quiet);
492 assert!(!renderer.render_quiet(&value));
493 }
494
495 #[test]
496 fn test_render_quiet_array_with_nulls() {
497 let value = json!([null, null]);
498 let renderer = OutputRenderer::new(OutputFormat::Quiet);
499 assert!(!renderer.render_quiet(&value));
500 }
501
502 #[derive(Serialize)]
503 struct TestStruct {
504 id: String,
505 name: String,
506 count: i32,
507 }
508
509 #[test]
510 fn test_render_json() {
511 let test_data = TestStruct {
512 id: "1".to_string(),
513 name: "Test".to_string(),
514 count: 42,
515 };
516
517 let renderer = OutputRenderer::new(OutputFormat::Json);
518 let result = renderer.render(&test_data);
519 assert!(result.is_ok());
520 }
521
522 #[test]
523 fn test_render_yaml() {
524 let test_data = TestStruct {
525 id: "1".to_string(),
526 name: "Test".to_string(),
527 count: 42,
528 };
529
530 let renderer = OutputRenderer::new(OutputFormat::Yaml);
531 let result = renderer.render(&test_data);
532 assert!(result.is_ok());
533 }
534
535 #[test]
536 fn test_render_table() {
537 let test_data = vec![
538 TestStruct {
539 id: "1".to_string(),
540 name: "Alice".to_string(),
541 count: 10,
542 },
543 TestStruct {
544 id: "2".to_string(),
545 name: "Bob".to_string(),
546 count: 20,
547 },
548 ];
549
550 let renderer = OutputRenderer::new(OutputFormat::Table);
551 let result = renderer.render(&test_data);
552 assert!(result.is_ok());
553 }
554
555 #[test]
556 fn test_render_csv() {
557 let test_data = vec![
558 TestStruct {
559 id: "1".to_string(),
560 name: "Alice".to_string(),
561 count: 10,
562 },
563 TestStruct {
564 id: "2".to_string(),
565 name: "Bob".to_string(),
566 count: 20,
567 },
568 ];
569
570 let renderer = OutputRenderer::new(OutputFormat::Csv);
571 let result = renderer.render(&test_data);
572 assert!(result.is_ok());
573 }
574
575 #[test]
576 fn test_render_markdown_table() {
577 let test_data = vec![
578 TestStruct {
579 id: "1".to_string(),
580 name: "Alice".to_string(),
581 count: 10,
582 },
583 TestStruct {
584 id: "2".to_string(),
585 name: "Bob".to_string(),
586 count: 20,
587 },
588 ];
589
590 let renderer = OutputRenderer::new(OutputFormat::Markdown);
591 let result = renderer.render(&test_data);
592 assert!(result.is_ok());
593 }
594
595 #[test]
596 fn test_render_markdown_single_object() {
597 let test_data = TestStruct {
598 id: "1".to_string(),
599 name: "Test".to_string(),
600 count: 42,
601 };
602
603 let renderer = OutputRenderer::new(OutputFormat::Markdown);
604 let result = renderer.render(&test_data);
605 assert!(result.is_ok());
606 }
607
608 #[test]
612 fn test_csv_field_quotes_per_rfc4180() {
613 assert_eq!(OutputRenderer::csv_field("plain"), "plain");
614 assert_eq!(OutputRenderer::csv_field("a,b"), "\"a,b\"");
615 assert_eq!(
616 OutputRenderer::csv_field("say \"hi\""),
617 "\"say \"\"hi\"\"\""
618 );
619 assert_eq!(
620 OutputRenderer::csv_field("line1\nline2"),
621 "\"line1\nline2\""
622 );
623 assert_eq!(OutputRenderer::csv_field("cr\r"), "\"cr\r\"");
624 assert_eq!(OutputRenderer::csv_field("no-specials"), "no-specials");
626 }
627
628 #[test]
629 fn test_csv_record_keeps_columns_aligned() {
630 let fields = vec![
631 "1".to_string(),
632 "Fix bug, urgently".to_string(),
633 "open".to_string(),
634 ];
635 assert_eq!(
637 OutputRenderer::csv_record(&fields),
638 "1,\"Fix bug, urgently\",open"
639 );
640 }
641
642 #[test]
644 fn test_markdown_cell_escapes_newlines_and_pipes() {
645 assert_eq!(OutputRenderer::markdown_cell("a|b"), "a\\|b");
646 assert_eq!(OutputRenderer::markdown_cell("one\ntwo"), "one<br>two");
647 assert_eq!(OutputRenderer::markdown_cell("one\r\ntwo"), "one<br>two");
648 assert_eq!(OutputRenderer::markdown_cell("plain"), "plain");
649 }
650
651 #[test]
652 fn test_render_markdown_pipe_escaping() {
653 let value = json!([
654 {"col": "a|b", "val": "x|y"}
655 ]);
656 let renderer = OutputRenderer::new(OutputFormat::Markdown);
657 assert!(renderer.render_markdown_table(&value).unwrap());
659 }
660
661 #[test]
662 fn test_render_raw() {
663 let renderer = OutputRenderer::new(OutputFormat::Markdown);
664 let result = renderer.render_raw("# Hello\n\nWorld");
665 assert!(result.is_ok());
666 }
667
668 #[test]
669 fn test_render_list_without_envelope() {
670 let data = vec![TestStruct {
671 id: "1".to_string(),
672 name: "Alice".to_string(),
673 count: 10,
674 }];
675 let renderer = OutputRenderer::new(OutputFormat::Table);
677 let result = renderer.render_list(&data);
678 assert!(result.is_ok());
679 }
680
681 #[test]
682 fn test_render_list_with_envelope() {
683 let data = vec![TestStruct {
684 id: "1".to_string(),
685 name: "Alice".to_string(),
686 count: 10,
687 }];
688 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
689 let result = renderer.render_list(&data);
691 assert!(result.is_ok());
692 }
693
694 #[test]
695 fn test_render_list_empty_with_envelope() {
696 let data: Vec<TestStruct> = vec![];
697 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
698 let result = renderer.render_list(&data);
699 assert!(result.is_ok());
700 }
701
702 #[test]
703 fn test_with_envelope_setter() {
704 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
705 assert_eq!(renderer.format(), OutputFormat::Json);
706 }
707
708 #[derive(Serialize)]
713 struct EmptyRow {
714 id: String,
715 }
716
717 #[test]
720 fn test_render_list_or_empty_json_emits_an_array() {
721 let renderer = OutputRenderer::new(OutputFormat::Json);
722 let rows: Vec<EmptyRow> = Vec::new();
723 assert!(renderer
726 .render_list_or_empty(&rows, "No rows found")
727 .is_ok());
728 }
729
730 #[test]
731 fn test_render_list_or_empty_is_a_message_only_for_humans() {
732 let rows: Vec<EmptyRow> = Vec::new();
733 for format in [OutputFormat::Table, OutputFormat::Markdown] {
734 let renderer = OutputRenderer::new(format);
735 assert!(renderer
736 .render_list_or_empty(&rows, "No rows found")
737 .is_ok());
738 }
739 for format in [
740 OutputFormat::Json,
741 OutputFormat::Yaml,
742 OutputFormat::Csv,
743 OutputFormat::Quiet,
744 ] {
745 let renderer = OutputRenderer::new(format);
746 assert!(renderer
747 .render_list_or_empty(&rows, "No rows found")
748 .is_ok());
749 }
750 }
751
752 #[test]
754 fn test_render_list_or_empty_renders_rows_when_present() {
755 let renderer = OutputRenderer::new(OutputFormat::Json);
756 let rows = vec![EmptyRow {
757 id: "1".to_string(),
758 }];
759 assert!(renderer
760 .render_list_or_empty(&rows, "No rows found")
761 .is_ok());
762 }
763
764 #[test]
767 fn test_render_list_or_empty_honours_the_envelope() {
768 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
769 let rows: Vec<EmptyRow> = Vec::new();
770 assert!(renderer
771 .render_list_or_empty(&rows, "No rows found")
772 .is_ok());
773 }
774}