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(Debug, Clone, Default)]
36pub struct ListMeta {
37 pub total: Option<u64>,
43 pub truncated: Option<bool>,
51 pub next: Option<String>,
54}
55
56impl ListMeta {
57 pub fn unknown() -> Self {
63 Self::default()
64 }
65
66 pub fn known(total: Option<u64>, truncated: bool, next: Option<String>) -> Self {
68 Self {
69 total,
70 truncated: Some(truncated),
71 next,
72 }
73 }
74}
75
76#[derive(Serialize)]
83struct ListEnvelope<'a, T: Serialize> {
84 data: &'a [T],
85 count: usize,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 total: Option<u64>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 truncated: Option<bool>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 next: Option<&'a str>,
93}
94
95impl<'a, T: Serialize> ListEnvelope<'a, T> {
96 fn new(items: &'a [T], meta: &'a ListMeta) -> Self {
97 Self {
98 data: items,
99 count: items.len(),
100 total: meta.total,
101 truncated: meta.truncated,
102 next: meta.next.as_deref(),
103 }
104 }
105}
106
107impl OutputRenderer {
108 pub fn new(format: OutputFormat) -> Self {
109 Self {
110 format,
111 envelope: false,
112 }
113 }
114
115 pub fn with_envelope(mut self, envelope: bool) -> Self {
116 self.envelope = envelope;
117 self
118 }
119
120 pub fn format(&self) -> OutputFormat {
121 self.format
122 }
123
124 pub fn render<T: Serialize>(&self, value: &T) -> Result<()> {
125 let json_value = serde_json::to_value(value)?;
126
127 match self.format {
128 OutputFormat::Table => {
129 if !self.render_table(&json_value)? {
130 println!("{}", serde_json::to_string_pretty(&json_value)?);
131 }
132 }
133 OutputFormat::Json => {
134 println!("{}", serde_json::to_string_pretty(&json_value)?);
135 }
136 OutputFormat::Yaml => {
137 println!("{}", serde_yaml::to_string(&json_value)?);
138 }
139 OutputFormat::Csv => {
140 if !self.render_csv(&json_value)? {
141 println!("{}", serde_json::to_string_pretty(&json_value)?);
142 }
143 }
144 OutputFormat::Quiet => {
145 if !self.render_quiet(&json_value) {
146 println!("{}", serde_json::to_string_pretty(&json_value)?);
147 }
148 }
149 OutputFormat::Markdown => {
150 if !self.render_markdown_table(&json_value)? {
151 self.render_markdown_single(&json_value)?;
152 }
153 }
154 }
155
156 Ok(())
157 }
158
159 pub fn render_list<T: Serialize>(&self, items: &[T]) -> Result<()> {
162 self.render_list_with_meta(items, &ListMeta::unknown())
163 }
164
165 pub fn render_list_with_meta<T: Serialize>(&self, items: &[T], meta: &ListMeta) -> Result<()> {
172 if self.envelope {
173 match self.format {
174 OutputFormat::Json => {
175 let envelope = ListEnvelope::new(items, meta);
176 println!("{}", serde_json::to_string_pretty(&envelope)?);
177 return Ok(());
178 }
179 OutputFormat::Yaml => {
180 let envelope = ListEnvelope::new(items, meta);
181 println!("{}", serde_yaml::to_string(&envelope)?);
182 return Ok(());
183 }
184 _ => {}
185 }
186 }
187 if items.is_empty() {
188 match self.format {
189 OutputFormat::Quiet | OutputFormat::Csv => return Ok(()),
194 _ => {}
195 }
196 }
197 self.render(&items)
198 }
199
200 pub fn render_list_or_empty<T: Serialize>(
207 &self,
208 items: &[T],
209 empty_message: &str,
210 ) -> Result<()> {
211 if items.is_empty() && matches!(self.format, OutputFormat::Table | OutputFormat::Markdown) {
215 println!("{empty_message}");
216 return Ok(());
217 }
218 self.render_list(items)
219 }
220
221 pub fn render_rows_ordered(&self, rows: &[Value], columns: &[String]) -> Result<()> {
233 let value = Value::Array(rows.to_vec());
234
235 match self.format {
236 OutputFormat::Table => {
237 if !self.render_table_with(&value, Some(columns))? {
238 println!("{}", serde_json::to_string_pretty(&value)?);
239 }
240 }
241 OutputFormat::Csv => {
242 if !self.render_csv_with(&value, Some(columns))? {
243 println!("{}", serde_json::to_string_pretty(&value)?);
244 }
245 }
246 OutputFormat::Markdown => {
247 if !self.render_markdown_table_with(&value, Some(columns))? {
248 self.render_markdown_single(&value)?;
249 }
250 }
251 _ => self.render(&value)?,
252 }
253
254 Ok(())
255 }
256
257 fn render_table(&self, value: &Value) -> Result<bool> {
258 self.render_table_with(value, None)
259 }
260
261 fn render_table_with(&self, value: &Value, columns: Option<&[String]>) -> Result<bool> {
262 let (headers, rows) = match Self::coerce_rows_with(value, columns) {
263 Some(data) => data,
264 None => return Ok(false),
265 };
266
267 let mut builder = Builder::default();
268 builder.push_record(headers);
269 for row in rows {
270 builder.push_record(row);
271 }
272
273 let table = builder.build().with(Style::rounded()).to_string();
274 println!("{}", table);
275 Ok(true)
276 }
277
278 fn render_csv(&self, value: &Value) -> Result<bool> {
279 self.render_csv_with(value, None)
280 }
281
282 fn render_csv_with(&self, value: &Value, columns: Option<&[String]>) -> Result<bool> {
283 let (headers, rows) = match Self::coerce_rows_with(value, columns) {
284 Some(data) => data,
285 None => return Ok(false),
286 };
287
288 println!("{}", Self::csv_record(&headers));
289 for row in rows {
290 println!("{}", Self::csv_record(&row));
291 }
292
293 Ok(true)
294 }
295
296 fn csv_record(fields: &[String]) -> String {
303 fields
304 .iter()
305 .map(|f| Self::csv_field(f))
306 .collect::<Vec<_>>()
307 .join(",")
308 }
309
310 fn csv_field(field: &str) -> String {
311 if field.contains([',', '"', '\n', '\r']) {
312 format!("\"{}\"", field.replace('"', "\"\""))
313 } else {
314 field.to_string()
315 }
316 }
317
318 fn render_quiet(&self, value: &Value) -> bool {
319 match value {
320 Value::Array(rows) => {
321 let mut printed = false;
322 for row in rows {
323 if let Value::Object(obj) = row {
324 if let Some(id) = obj.get("id").and_then(Value::as_str) {
325 println!("{id}");
326 printed = true;
327 } else if let Some(key) = obj.keys().next() {
328 if let Some(val) = obj.get(key) {
329 println!("{}", val);
330 printed = true;
331 }
332 }
333 } else if !row.is_null() {
334 println!("{}", row);
335 printed = true;
336 }
337 }
338 printed
339 }
340 Value::Object(obj) => {
341 if let Some(id) = obj.get("id").and_then(Value::as_str) {
342 println!("{id}");
343 true
344 } else {
345 false
346 }
347 }
348 Value::Null => false,
349 other => {
350 println!("{}", other);
351 true
352 }
353 }
354 }
355
356 pub fn render_raw(&self, content: &str) -> Result<()> {
358 println!("{content}");
359 Ok(())
360 }
361
362 fn render_markdown_table(&self, value: &Value) -> Result<bool> {
363 self.render_markdown_table_with(value, None)
364 }
365
366 fn render_markdown_table_with(
367 &self,
368 value: &Value,
369 columns: Option<&[String]>,
370 ) -> Result<bool> {
371 let (headers, rows) = match Self::coerce_rows_with(value, columns) {
372 Some(data) => data,
373 None => return Ok(false),
374 };
375
376 let header_line: String = headers
378 .iter()
379 .map(|h| Self::markdown_cell(h))
380 .collect::<Vec<_>>()
381 .join(" | ");
382 println!("| {} |", header_line);
383
384 let separator: String = headers
386 .iter()
387 .map(|_| "---")
388 .collect::<Vec<_>>()
389 .join(" | ");
390 println!("| {} |", separator);
391
392 for row in rows {
394 let cells: String = row
395 .iter()
396 .map(|c| Self::markdown_cell(c))
397 .collect::<Vec<_>>()
398 .join(" | ");
399 println!("| {} |", cells);
400 }
401
402 Ok(true)
403 }
404
405 fn markdown_cell(cell: &str) -> String {
411 cell.replace('|', "\\|")
412 .replace("\r\n", "<br>")
413 .replace(['\n', '\r'], "<br>")
414 }
415
416 fn render_markdown_single(&self, value: &Value) -> Result<bool> {
417 if let Value::Object(obj) = value {
418 for (key, val) in obj {
419 let display = Self::value_to_string(val);
420 println!("**{}**: {}", key, display);
421 }
422 Ok(true)
423 } else {
424 println!("{}", serde_json::to_string_pretty(value)?);
425 Ok(true)
426 }
427 }
428
429 #[cfg(test)]
432 fn coerce_rows(value: &Value) -> Option<(Vec<String>, Vec<Vec<String>>)> {
433 Self::coerce_rows_with(value, None)
434 }
435
436 fn coerce_rows_with(
443 value: &Value,
444 columns: Option<&[String]>,
445 ) -> Option<(Vec<String>, Vec<Vec<String>>)> {
446 let rows = match value {
447 Value::Array(rows) if !rows.is_empty() => rows,
448 _ => return None,
449 };
450
451 let headers_vec: Vec<String> = match columns {
452 Some(columns) => columns.to_vec(),
453 None => {
454 let mut headers = BTreeSet::new();
455 for row in rows {
456 if let Value::Object(obj) = row {
457 headers.extend(obj.keys().cloned());
458 }
459 }
460 headers.into_iter().collect()
461 }
462 };
463
464 if headers_vec.is_empty() {
465 return None;
466 }
467
468 let mut data = Vec::with_capacity(rows.len());
469 for row in rows {
470 let mut record = Vec::with_capacity(headers_vec.len());
471 if let Value::Object(obj) = row {
472 for header in &headers_vec {
473 let cell = obj
474 .get(header)
475 .map(Self::value_to_string)
476 .unwrap_or_else(|| "".to_string());
477 record.push(cell);
478 }
479 }
480 data.push(record);
481 }
482
483 Some((headers_vec, data))
484 }
485
486 fn value_to_string(value: &Value) -> String {
487 match value {
488 Value::String(s) => s.clone(),
489 Value::Number(n) => n.to_string(),
490 Value::Bool(b) => b.to_string(),
491 Value::Null => String::new(),
492 other => serde_json::to_string(other).unwrap_or_default(),
493 }
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use serde_json::json;
501
502 #[test]
503 fn test_output_format_default() {
504 assert_eq!(OutputFormat::default(), OutputFormat::Table);
505 }
506
507 #[test]
508 fn test_renderer_new() {
509 let renderer = OutputRenderer::new(OutputFormat::Json);
510 assert_eq!(renderer.format(), OutputFormat::Json);
511 }
512
513 #[test]
514 fn test_coerce_rows_empty_array() {
515 let value = json!([]);
516 assert!(OutputRenderer::coerce_rows(&value).is_none());
517 }
518
519 #[test]
520 fn test_coerce_rows_single_object() {
521 let value = json!([
522 {"id": "1", "name": "Alice"},
523 {"id": "2", "name": "Bob"}
524 ]);
525
526 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
527 assert_eq!(headers.len(), 2);
528 assert!(headers.contains(&"id".to_string()));
529 assert!(headers.contains(&"name".to_string()));
530 assert_eq!(rows.len(), 2);
531 }
532
533 #[test]
534 fn test_coerce_rows_mixed_keys() {
535 let value = json!([
536 {"id": "1", "name": "Alice"},
537 {"id": "2", "email": "bob@example.com"}
538 ]);
539
540 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
541 assert_eq!(headers.len(), 3);
542 assert!(headers.contains(&"id".to_string()));
543 assert!(headers.contains(&"name".to_string()));
544 assert!(headers.contains(&"email".to_string()));
545
546 assert_eq!(
547 rows[0][headers.iter().position(|h| h == "id").unwrap()],
548 "1"
549 );
550 assert_eq!(
551 rows[0][headers.iter().position(|h| h == "name").unwrap()],
552 "Alice"
553 );
554 assert_eq!(
555 rows[0][headers.iter().position(|h| h == "email").unwrap()],
556 ""
557 );
558 }
559
560 #[test]
561 fn test_coerce_rows_not_array() {
562 let value = json!({"id": "1", "name": "Alice"});
563 assert!(OutputRenderer::coerce_rows(&value).is_none());
564 }
565
566 #[test]
569 fn coerce_rows_without_columns_sorts_headers_alphabetically() {
570 let value = json!([{"zebra": "1", "apple": "2"}]);
571 let (headers, _) = OutputRenderer::coerce_rows(&value).unwrap();
572 assert_eq!(headers, vec!["apple".to_string(), "zebra".to_string()]);
573 }
574
575 #[test]
577 fn coerce_rows_with_columns_preserves_the_given_order() {
578 let value = json!([{"apple": "2", "zebra": "1"}]);
579 let columns = vec!["zebra".to_string(), "apple".to_string()];
580
581 let (headers, rows) = OutputRenderer::coerce_rows_with(&value, Some(&columns)).unwrap();
582
583 assert_eq!(headers, columns);
584 assert_eq!(rows[0], vec!["1".to_string(), "2".to_string()]);
585 }
586
587 #[test]
588 fn coerce_rows_with_columns_drops_keys_not_listed() {
589 let value = json!([{"wanted": "yes", "unwanted": "no"}]);
590 let columns = vec!["wanted".to_string()];
591
592 let (headers, rows) = OutputRenderer::coerce_rows_with(&value, Some(&columns)).unwrap();
593
594 assert_eq!(headers, columns);
595 assert_eq!(rows[0], vec!["yes".to_string()]);
596 }
597
598 #[test]
601 fn coerce_rows_with_columns_fills_absent_keys_with_empty() {
602 let value = json!([{"present": "here"}]);
603 let columns = vec!["present".to_string(), "absent".to_string()];
604
605 let (_, rows) = OutputRenderer::coerce_rows_with(&value, Some(&columns)).unwrap();
606
607 assert_eq!(rows[0], vec!["here".to_string(), String::new()]);
608 }
609
610 #[test]
611 fn coerce_rows_with_empty_columns_renders_nothing() {
612 let value = json!([{"id": "1"}]);
613 assert!(OutputRenderer::coerce_rows_with(&value, Some(&[])).is_none());
614 }
615
616 #[test]
617 fn test_coerce_rows_array_of_primitives() {
618 let value = json!(["one", "two", "three"]);
619 assert!(OutputRenderer::coerce_rows(&value).is_none());
620 }
621
622 #[test]
623 fn test_value_to_string_string() {
624 let value = json!("hello");
625 assert_eq!(OutputRenderer::value_to_string(&value), "hello");
626 }
627
628 #[test]
629 fn test_value_to_string_number() {
630 let value = json!(42);
631 assert_eq!(OutputRenderer::value_to_string(&value), "42");
632 }
633
634 #[test]
635 fn test_value_to_string_bool() {
636 let value = json!(true);
637 assert_eq!(OutputRenderer::value_to_string(&value), "true");
638 }
639
640 #[test]
641 fn test_value_to_string_null() {
642 let value = json!(null);
643 assert_eq!(OutputRenderer::value_to_string(&value), "");
644 }
645
646 #[test]
647 fn test_value_to_string_object() {
648 let value = json!({"key": "value"});
649 let result = OutputRenderer::value_to_string(&value);
650 assert!(result.contains("key"));
651 assert!(result.contains("value"));
652 }
653
654 #[test]
655 fn test_render_quiet_object_with_id() {
656 let value = json!({"id": "123", "name": "Test"});
657 let renderer = OutputRenderer::new(OutputFormat::Quiet);
658 assert!(renderer.render_quiet(&value));
659 }
660
661 #[test]
662 fn test_render_quiet_object_without_id() {
663 let value = json!({"name": "Test"});
664 let renderer = OutputRenderer::new(OutputFormat::Quiet);
665 assert!(!renderer.render_quiet(&value));
666 }
667
668 #[test]
669 fn test_render_quiet_array_with_ids() {
670 let value = json!([
671 {"id": "1", "name": "Alice"},
672 {"id": "2", "name": "Bob"}
673 ]);
674 let renderer = OutputRenderer::new(OutputFormat::Quiet);
675 assert!(renderer.render_quiet(&value));
676 }
677
678 #[test]
679 fn test_render_quiet_primitive() {
680 let value = json!("simple");
681 let renderer = OutputRenderer::new(OutputFormat::Quiet);
682 assert!(renderer.render_quiet(&value));
683 }
684
685 #[test]
686 fn test_render_quiet_null() {
687 let value = json!(null);
688 let renderer = OutputRenderer::new(OutputFormat::Quiet);
689 assert!(!renderer.render_quiet(&value));
690 }
691
692 #[test]
693 fn test_render_quiet_array_with_nulls() {
694 let value = json!([null, null]);
695 let renderer = OutputRenderer::new(OutputFormat::Quiet);
696 assert!(!renderer.render_quiet(&value));
697 }
698
699 #[derive(Serialize)]
700 struct TestStruct {
701 id: String,
702 name: String,
703 count: i32,
704 }
705
706 #[test]
707 fn test_render_json() {
708 let test_data = TestStruct {
709 id: "1".to_string(),
710 name: "Test".to_string(),
711 count: 42,
712 };
713
714 let renderer = OutputRenderer::new(OutputFormat::Json);
715 let result = renderer.render(&test_data);
716 assert!(result.is_ok());
717 }
718
719 #[test]
720 fn test_render_yaml() {
721 let test_data = TestStruct {
722 id: "1".to_string(),
723 name: "Test".to_string(),
724 count: 42,
725 };
726
727 let renderer = OutputRenderer::new(OutputFormat::Yaml);
728 let result = renderer.render(&test_data);
729 assert!(result.is_ok());
730 }
731
732 #[test]
733 fn test_render_table() {
734 let test_data = vec![
735 TestStruct {
736 id: "1".to_string(),
737 name: "Alice".to_string(),
738 count: 10,
739 },
740 TestStruct {
741 id: "2".to_string(),
742 name: "Bob".to_string(),
743 count: 20,
744 },
745 ];
746
747 let renderer = OutputRenderer::new(OutputFormat::Table);
748 let result = renderer.render(&test_data);
749 assert!(result.is_ok());
750 }
751
752 #[test]
753 fn test_render_csv() {
754 let test_data = vec![
755 TestStruct {
756 id: "1".to_string(),
757 name: "Alice".to_string(),
758 count: 10,
759 },
760 TestStruct {
761 id: "2".to_string(),
762 name: "Bob".to_string(),
763 count: 20,
764 },
765 ];
766
767 let renderer = OutputRenderer::new(OutputFormat::Csv);
768 let result = renderer.render(&test_data);
769 assert!(result.is_ok());
770 }
771
772 #[test]
773 fn test_render_markdown_table() {
774 let test_data = vec![
775 TestStruct {
776 id: "1".to_string(),
777 name: "Alice".to_string(),
778 count: 10,
779 },
780 TestStruct {
781 id: "2".to_string(),
782 name: "Bob".to_string(),
783 count: 20,
784 },
785 ];
786
787 let renderer = OutputRenderer::new(OutputFormat::Markdown);
788 let result = renderer.render(&test_data);
789 assert!(result.is_ok());
790 }
791
792 #[test]
793 fn test_render_markdown_single_object() {
794 let test_data = TestStruct {
795 id: "1".to_string(),
796 name: "Test".to_string(),
797 count: 42,
798 };
799
800 let renderer = OutputRenderer::new(OutputFormat::Markdown);
801 let result = renderer.render(&test_data);
802 assert!(result.is_ok());
803 }
804
805 #[test]
809 fn test_csv_field_quotes_per_rfc4180() {
810 assert_eq!(OutputRenderer::csv_field("plain"), "plain");
811 assert_eq!(OutputRenderer::csv_field("a,b"), "\"a,b\"");
812 assert_eq!(
813 OutputRenderer::csv_field("say \"hi\""),
814 "\"say \"\"hi\"\"\""
815 );
816 assert_eq!(
817 OutputRenderer::csv_field("line1\nline2"),
818 "\"line1\nline2\""
819 );
820 assert_eq!(OutputRenderer::csv_field("cr\r"), "\"cr\r\"");
821 assert_eq!(OutputRenderer::csv_field("no-specials"), "no-specials");
823 }
824
825 #[test]
826 fn test_csv_record_keeps_columns_aligned() {
827 let fields = vec![
828 "1".to_string(),
829 "Fix bug, urgently".to_string(),
830 "open".to_string(),
831 ];
832 assert_eq!(
834 OutputRenderer::csv_record(&fields),
835 "1,\"Fix bug, urgently\",open"
836 );
837 }
838
839 #[test]
841 fn test_markdown_cell_escapes_newlines_and_pipes() {
842 assert_eq!(OutputRenderer::markdown_cell("a|b"), "a\\|b");
843 assert_eq!(OutputRenderer::markdown_cell("one\ntwo"), "one<br>two");
844 assert_eq!(OutputRenderer::markdown_cell("one\r\ntwo"), "one<br>two");
845 assert_eq!(OutputRenderer::markdown_cell("plain"), "plain");
846 }
847
848 #[test]
849 fn test_render_markdown_pipe_escaping() {
850 let value = json!([
851 {"col": "a|b", "val": "x|y"}
852 ]);
853 let renderer = OutputRenderer::new(OutputFormat::Markdown);
854 assert!(renderer.render_markdown_table(&value).unwrap());
856 }
857
858 #[test]
859 fn test_render_raw() {
860 let renderer = OutputRenderer::new(OutputFormat::Markdown);
861 let result = renderer.render_raw("# Hello\n\nWorld");
862 assert!(result.is_ok());
863 }
864
865 #[test]
866 fn test_render_list_without_envelope() {
867 let data = vec![TestStruct {
868 id: "1".to_string(),
869 name: "Alice".to_string(),
870 count: 10,
871 }];
872 let renderer = OutputRenderer::new(OutputFormat::Table);
874 let result = renderer.render_list(&data);
875 assert!(result.is_ok());
876 }
877
878 #[test]
879 fn test_render_list_with_envelope() {
880 let data = vec![TestStruct {
881 id: "1".to_string(),
882 name: "Alice".to_string(),
883 count: 10,
884 }];
885 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
886 let result = renderer.render_list(&data);
888 assert!(result.is_ok());
889 }
890
891 #[test]
892 fn test_render_list_empty_with_envelope() {
893 let data: Vec<TestStruct> = vec![];
894 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
895 let result = renderer.render_list(&data);
896 assert!(result.is_ok());
897 }
898
899 #[test]
900 fn test_with_envelope_setter() {
901 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
902 assert_eq!(renderer.format(), OutputFormat::Json);
903 }
904
905 #[derive(Serialize)]
910 struct EmptyRow {
911 id: String,
912 }
913
914 #[test]
917 fn test_render_list_or_empty_json_emits_an_array() {
918 let renderer = OutputRenderer::new(OutputFormat::Json);
919 let rows: Vec<EmptyRow> = Vec::new();
920 assert!(renderer
923 .render_list_or_empty(&rows, "No rows found")
924 .is_ok());
925 }
926
927 #[test]
928 fn test_render_list_or_empty_is_a_message_only_for_humans() {
929 let rows: Vec<EmptyRow> = Vec::new();
930 for format in [OutputFormat::Table, OutputFormat::Markdown] {
931 let renderer = OutputRenderer::new(format);
932 assert!(renderer
933 .render_list_or_empty(&rows, "No rows found")
934 .is_ok());
935 }
936 for format in [
937 OutputFormat::Json,
938 OutputFormat::Yaml,
939 OutputFormat::Csv,
940 OutputFormat::Quiet,
941 ] {
942 let renderer = OutputRenderer::new(format);
943 assert!(renderer
944 .render_list_or_empty(&rows, "No rows found")
945 .is_ok());
946 }
947 }
948
949 #[test]
951 fn test_render_list_or_empty_renders_rows_when_present() {
952 let renderer = OutputRenderer::new(OutputFormat::Json);
953 let rows = vec![EmptyRow {
954 id: "1".to_string(),
955 }];
956 assert!(renderer
957 .render_list_or_empty(&rows, "No rows found")
958 .is_ok());
959 }
960
961 #[test]
964 fn test_render_list_or_empty_honours_the_envelope() {
965 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
966 let rows: Vec<EmptyRow> = Vec::new();
967 assert!(renderer
968 .render_list_or_empty(&rows, "No rows found")
969 .is_ok());
970 }
971}