1use std::collections::BTreeSet;
2
3use anyhow::Result;
4use clap::ValueEnum;
5use indexmap::IndexMap;
6use serde::Serialize;
7use serde_json::Value;
8use tabled::builder::Builder;
9use tabled::settings::object::Columns;
10use tabled::settings::{Style, Width};
11
12pub mod colors;
13
14pub use colors::StatusFormatter;
15
16#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum, Default)]
17pub enum OutputFormat {
18 #[default]
19 Table,
20 Json,
21 Yaml,
22 Csv,
23 Quiet,
24 Markdown,
25}
26
27impl OutputFormat {
28 pub fn is_human(self) -> bool {
34 matches!(self, OutputFormat::Table | OutputFormat::Markdown)
35 }
36}
37
38const RECORD_VALUE_WIDTH: usize = 100;
42
43pub struct OutputRenderer {
44 format: OutputFormat,
45 envelope: bool,
46}
47
48#[derive(Debug, Clone, Default)]
54pub struct ListMeta {
55 pub total: Option<u64>,
61 pub truncated: Option<bool>,
69 pub next: Option<String>,
72}
73
74impl ListMeta {
75 pub fn unknown() -> Self {
81 Self::default()
82 }
83
84 pub fn known(total: Option<u64>, truncated: bool, next: Option<String>) -> Self {
86 Self {
87 total,
88 truncated: Some(truncated),
89 next,
90 }
91 }
92}
93
94#[derive(Serialize)]
101struct ListEnvelope<'a, T: Serialize> {
102 data: &'a [T],
103 count: usize,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 total: Option<u64>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 truncated: Option<bool>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 next: Option<&'a str>,
111}
112
113impl<'a, T: Serialize> ListEnvelope<'a, T> {
114 fn new(items: &'a [T], meta: &'a ListMeta) -> Self {
115 Self {
116 data: items,
117 count: items.len(),
118 total: meta.total,
119 truncated: meta.truncated,
120 next: meta.next.as_deref(),
121 }
122 }
123}
124
125impl OutputRenderer {
126 pub fn new(format: OutputFormat) -> Self {
127 Self {
128 format,
129 envelope: false,
130 }
131 }
132
133 pub fn with_envelope(mut self, envelope: bool) -> Self {
134 self.envelope = envelope;
135 self
136 }
137
138 pub fn format(&self) -> OutputFormat {
139 self.format
140 }
141
142 pub fn render<T: Serialize>(&self, value: &T) -> Result<()> {
143 let json_value = serde_json::to_value(value)?;
144
145 match self.format {
146 OutputFormat::Table => {
147 println!("{}", Self::table_output(value, &json_value)?);
148 }
149 OutputFormat::Json => {
150 println!("{}", serde_json::to_string_pretty(&json_value)?);
151 }
152 OutputFormat::Yaml => {
153 println!("{}", serde_yaml::to_string(&json_value)?);
154 }
155 OutputFormat::Csv => {
156 if !self.render_csv(&json_value)? {
157 println!("{}", serde_json::to_string_pretty(&json_value)?);
158 }
159 }
160 OutputFormat::Quiet => {
161 if !self.render_quiet(&json_value) {
162 println!("{}", serde_json::to_string_pretty(&json_value)?);
163 }
164 }
165 OutputFormat::Markdown => {
166 if !self.render_markdown_table(&json_value)? {
167 self.render_markdown_single(&json_value)?;
168 }
169 }
170 }
171
172 Ok(())
173 }
174
175 pub fn render_document<T: Serialize>(&self, value: &T) -> Result<()> {
181 match self.format {
182 OutputFormat::Table => {
183 println!("{}", serde_json::to_string_pretty(value)?);
184 Ok(())
185 }
186 _ => self.render(value),
187 }
188 }
189
190 fn table_output<T: Serialize>(value: &T, json_value: &Value) -> Result<String> {
199 if json_value.is_object() {
200 return Ok(Self::format_record(&Self::ordered_fields(
201 value, json_value,
202 )));
203 }
204 match Self::table_string(json_value, None) {
205 Some(table) => Ok(table),
206 None => Ok(serde_json::to_string_pretty(json_value)?),
207 }
208 }
209
210 fn ordered_fields<T: Serialize>(value: &T, json_value: &Value) -> Vec<(String, Value)> {
217 serde_json::to_string(value)
218 .ok()
219 .and_then(|text| serde_json::from_str::<IndexMap<String, Value>>(&text).ok())
220 .map(|fields| fields.into_iter().collect())
221 .unwrap_or_else(|| {
222 json_value
223 .as_object()
224 .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
225 .unwrap_or_default()
226 })
227 }
228
229 fn format_record(fields: &[(String, Value)]) -> String {
237 let mut builder = Builder::default();
238 builder.push_record(["field".to_string(), "value".to_string()]);
239 let mut sections = Vec::new();
240 for (key, value) in fields {
241 match value {
242 Value::Array(items) if !items.is_empty() && items.iter().all(Value::is_object) => {
243 sections.push((key, value));
244 }
245 Value::Array(items) if items.iter().all(|v| !v.is_object() && !v.is_array()) => {
246 let joined = items
247 .iter()
248 .map(Self::value_to_string)
249 .collect::<Vec<_>>()
250 .join(", ");
251 builder.push_record([key.clone(), joined]);
252 }
253 other => builder.push_record([key.clone(), Self::value_to_string(other)]),
254 }
255 }
256
257 let mut table = builder.build();
258 table.with(Style::rounded()).modify(
259 Columns::one(1),
260 Width::wrap(RECORD_VALUE_WIDTH).keep_words(true),
261 );
262 let mut out = table.to_string();
263 for (key, value) in sections {
264 if let Some(sub) = Self::table_string(value, None) {
265 out.push_str(&format!("\n\n{key}:\n{sub}"));
266 }
267 }
268 out
269 }
270
271 pub fn render_list<T: Serialize>(&self, items: &[T]) -> Result<()> {
274 self.render_list_with_meta(items, &ListMeta::unknown())
275 }
276
277 pub fn render_list_with_meta<T: Serialize>(&self, items: &[T], meta: &ListMeta) -> Result<()> {
284 if self.envelope {
285 match self.format {
286 OutputFormat::Json => {
287 let envelope = ListEnvelope::new(items, meta);
288 println!("{}", serde_json::to_string_pretty(&envelope)?);
289 return Ok(());
290 }
291 OutputFormat::Yaml => {
292 let envelope = ListEnvelope::new(items, meta);
293 println!("{}", serde_yaml::to_string(&envelope)?);
294 return Ok(());
295 }
296 _ => {}
297 }
298 }
299 if items.is_empty() {
300 match self.format {
301 OutputFormat::Quiet | OutputFormat::Csv => return Ok(()),
306 _ => {}
307 }
308 }
309 self.render(&items)
310 }
311
312 pub fn render_list_or_empty<T: Serialize>(
319 &self,
320 items: &[T],
321 empty_message: &str,
322 ) -> Result<()> {
323 if items.is_empty() && matches!(self.format, OutputFormat::Table | OutputFormat::Markdown) {
327 println!("{empty_message}");
328 return Ok(());
329 }
330 self.render_list(items)
331 }
332
333 pub fn render_rows_ordered(&self, rows: &[Value], columns: &[String]) -> Result<()> {
345 let value = Value::Array(rows.to_vec());
346
347 match self.format {
348 OutputFormat::Table => {
349 if !self.render_table_with(&value, Some(columns))? {
350 println!("{}", serde_json::to_string_pretty(&value)?);
351 }
352 }
353 OutputFormat::Csv => {
354 if !self.render_csv_with(&value, Some(columns))? {
355 println!("{}", serde_json::to_string_pretty(&value)?);
356 }
357 }
358 OutputFormat::Markdown => {
359 if !self.render_markdown_table_with(&value, Some(columns))? {
360 self.render_markdown_single(&value)?;
361 }
362 }
363 _ => self.render(&value)?,
364 }
365
366 Ok(())
367 }
368
369 fn render_table_with(&self, value: &Value, columns: Option<&[String]>) -> Result<bool> {
370 match Self::table_string(value, columns) {
371 Some(table) => {
372 println!("{}", table);
373 Ok(true)
374 }
375 None => Ok(false),
376 }
377 }
378
379 fn table_string(value: &Value, columns: Option<&[String]>) -> Option<String> {
381 let (headers, rows) = Self::coerce_rows_with(value, columns)?;
382
383 let mut builder = Builder::default();
384 builder.push_record(headers);
385 for row in rows {
386 builder.push_record(row);
387 }
388
389 Some(builder.build().with(Style::rounded()).to_string())
390 }
391
392 fn render_csv(&self, value: &Value) -> Result<bool> {
393 self.render_csv_with(value, None)
394 }
395
396 fn render_csv_with(&self, value: &Value, columns: Option<&[String]>) -> Result<bool> {
397 let (headers, rows) = match Self::coerce_rows_with(value, columns) {
398 Some(data) => data,
399 None => return Ok(false),
400 };
401
402 println!("{}", Self::csv_record(&headers));
403 for row in rows {
404 println!("{}", Self::csv_record(&row));
405 }
406
407 Ok(true)
408 }
409
410 fn csv_record(fields: &[String]) -> String {
417 fields
418 .iter()
419 .map(|f| Self::csv_field(f))
420 .collect::<Vec<_>>()
421 .join(",")
422 }
423
424 fn csv_field(field: &str) -> String {
425 if field.contains([',', '"', '\n', '\r']) {
426 format!("\"{}\"", field.replace('"', "\"\""))
427 } else {
428 field.to_string()
429 }
430 }
431
432 fn render_quiet(&self, value: &Value) -> bool {
433 match value {
434 Value::Array(rows) => {
435 let mut printed = false;
436 for row in rows {
437 if let Value::Object(obj) = row {
438 if let Some(id) = obj.get("id").and_then(Value::as_str) {
439 println!("{id}");
440 printed = true;
441 } else if let Some(key) = obj.keys().next() {
442 if let Some(val) = obj.get(key) {
443 println!("{}", val);
444 printed = true;
445 }
446 }
447 } else if !row.is_null() {
448 println!("{}", row);
449 printed = true;
450 }
451 }
452 printed
453 }
454 Value::Object(obj) => {
455 if let Some(id) = obj.get("id").and_then(Value::as_str) {
456 println!("{id}");
457 true
458 } else {
459 false
460 }
461 }
462 Value::Null => false,
463 other => {
464 println!("{}", other);
465 true
466 }
467 }
468 }
469
470 pub fn render_raw(&self, content: &str) -> Result<()> {
472 println!("{content}");
473 Ok(())
474 }
475
476 fn render_markdown_table(&self, value: &Value) -> Result<bool> {
477 self.render_markdown_table_with(value, None)
478 }
479
480 fn render_markdown_table_with(
481 &self,
482 value: &Value,
483 columns: Option<&[String]>,
484 ) -> Result<bool> {
485 let (headers, rows) = match Self::coerce_rows_with(value, columns) {
486 Some(data) => data,
487 None => return Ok(false),
488 };
489
490 let header_line: String = headers
492 .iter()
493 .map(|h| Self::markdown_cell(h))
494 .collect::<Vec<_>>()
495 .join(" | ");
496 println!("| {} |", header_line);
497
498 let separator: String = headers
500 .iter()
501 .map(|_| "---")
502 .collect::<Vec<_>>()
503 .join(" | ");
504 println!("| {} |", separator);
505
506 for row in rows {
508 let cells: String = row
509 .iter()
510 .map(|c| Self::markdown_cell(c))
511 .collect::<Vec<_>>()
512 .join(" | ");
513 println!("| {} |", cells);
514 }
515
516 Ok(true)
517 }
518
519 fn markdown_cell(cell: &str) -> String {
525 cell.replace('|', "\\|")
526 .replace("\r\n", "<br>")
527 .replace(['\n', '\r'], "<br>")
528 }
529
530 fn render_markdown_single(&self, value: &Value) -> Result<bool> {
531 if let Value::Object(obj) = value {
532 for (key, val) in obj {
533 let display = Self::value_to_string(val);
534 println!("**{}**: {}", key, display);
535 }
536 Ok(true)
537 } else {
538 println!("{}", serde_json::to_string_pretty(value)?);
539 Ok(true)
540 }
541 }
542
543 #[cfg(test)]
546 fn coerce_rows(value: &Value) -> Option<(Vec<String>, Vec<Vec<String>>)> {
547 Self::coerce_rows_with(value, None)
548 }
549
550 fn coerce_rows_with(
557 value: &Value,
558 columns: Option<&[String]>,
559 ) -> Option<(Vec<String>, Vec<Vec<String>>)> {
560 let rows = match value {
561 Value::Array(rows) if !rows.is_empty() => rows,
562 _ => return None,
563 };
564
565 let headers_vec: Vec<String> = match columns {
566 Some(columns) => columns.to_vec(),
567 None => {
568 let mut headers = BTreeSet::new();
569 for row in rows {
570 if let Value::Object(obj) = row {
571 headers.extend(obj.keys().cloned());
572 }
573 }
574 headers.into_iter().collect()
575 }
576 };
577
578 if headers_vec.is_empty() {
579 return None;
580 }
581
582 let mut data = Vec::with_capacity(rows.len());
583 for row in rows {
584 let mut record = Vec::with_capacity(headers_vec.len());
585 if let Value::Object(obj) = row {
586 for header in &headers_vec {
587 let cell = obj
588 .get(header)
589 .map(Self::value_to_string)
590 .unwrap_or_else(|| "".to_string());
591 record.push(cell);
592 }
593 }
594 data.push(record);
595 }
596
597 Some((headers_vec, data))
598 }
599
600 fn value_to_string(value: &Value) -> String {
601 match value {
602 Value::String(s) => s.clone(),
603 Value::Number(n) => n.to_string(),
604 Value::Bool(b) => b.to_string(),
605 Value::Null => String::new(),
606 other => serde_json::to_string(other).unwrap_or_default(),
607 }
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use serde_json::json;
615
616 #[test]
617 fn test_output_format_default() {
618 assert_eq!(OutputFormat::default(), OutputFormat::Table);
619 }
620
621 #[test]
622 fn test_renderer_new() {
623 let renderer = OutputRenderer::new(OutputFormat::Json);
624 assert_eq!(renderer.format(), OutputFormat::Json);
625 }
626
627 #[test]
628 fn test_coerce_rows_empty_array() {
629 let value = json!([]);
630 assert!(OutputRenderer::coerce_rows(&value).is_none());
631 }
632
633 #[test]
634 fn test_coerce_rows_single_object() {
635 let value = json!([
636 {"id": "1", "name": "Alice"},
637 {"id": "2", "name": "Bob"}
638 ]);
639
640 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
641 assert_eq!(headers.len(), 2);
642 assert!(headers.contains(&"id".to_string()));
643 assert!(headers.contains(&"name".to_string()));
644 assert_eq!(rows.len(), 2);
645 }
646
647 #[test]
648 fn test_coerce_rows_mixed_keys() {
649 let value = json!([
650 {"id": "1", "name": "Alice"},
651 {"id": "2", "email": "bob@example.com"}
652 ]);
653
654 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
655 assert_eq!(headers.len(), 3);
656 assert!(headers.contains(&"id".to_string()));
657 assert!(headers.contains(&"name".to_string()));
658 assert!(headers.contains(&"email".to_string()));
659
660 assert_eq!(
661 rows[0][headers.iter().position(|h| h == "id").unwrap()],
662 "1"
663 );
664 assert_eq!(
665 rows[0][headers.iter().position(|h| h == "name").unwrap()],
666 "Alice"
667 );
668 assert_eq!(
669 rows[0][headers.iter().position(|h| h == "email").unwrap()],
670 ""
671 );
672 }
673
674 #[derive(Serialize)]
675 struct PullRequestView {
676 id: i64,
677 title: &'static str,
678 approvals: String,
679 description: Option<&'static str>,
680 labels: Vec<&'static str>,
681 reviewers: Vec<serde_json::Value>,
682 }
683
684 fn pull_request() -> PullRequestView {
685 PullRequestView {
686 id: 54,
687 title: "Add image resizing",
688 approvals: "1".to_string(),
689 description: None,
690 labels: vec!["infra", "urgent"],
691 reviewers: vec![json!({"name": "Reviewer One", "status": "Approved", "uuid": "{r-1}"})],
692 }
693 }
694
695 fn table_of<T: Serialize>(value: &T) -> String {
696 let json_value = serde_json::to_value(value).unwrap();
697 OutputRenderer::table_output(value, &json_value).unwrap()
698 }
699
700 #[test]
702 fn a_single_object_renders_as_a_field_value_table() {
703 let out = table_of(&pull_request());
704 assert!(!out.trim_start().starts_with('{'), "not JSON: {out}");
705 assert!(out.contains("field") && out.contains("value"), "{out}");
706 assert!(out.contains("Add image resizing"), "{out}");
707 }
708
709 #[test]
711 fn fields_keep_their_declaration_order() {
712 let out = table_of(&pull_request());
713 let id = out.find("│ id").unwrap();
714 let title = out.find("│ title").unwrap();
715 let approvals = out.find("│ approvals").unwrap();
716 assert!(id < title && title < approvals, "{out}");
717 }
718
719 #[test]
720 fn a_list_of_objects_inside_becomes_a_titled_table_below() {
721 let out = table_of(&pull_request());
722 let main_end = out.find("reviewers:").expect("titled section");
723 let section = &out[main_end..];
724 for cell in [
725 "name",
726 "status",
727 "uuid",
728 "Reviewer One",
729 "Approved",
730 "{r-1}",
731 ] {
732 assert!(section.contains(cell), "{cell} missing from: {section}");
733 }
734 assert!(
735 !out[..main_end].contains("Reviewer One"),
736 "not squeezed into a cell"
737 );
738 }
739
740 #[test]
741 fn scalars_lists_and_nulls_render_plainly() {
742 let out = table_of(&pull_request());
743 assert!(out.contains("infra, urgent"), "{out}");
744 let description_line = out.lines().find(|l| l.contains("description")).unwrap();
745 assert!(!description_line.contains("null"), "{description_line}");
746 }
747
748 #[test]
749 fn a_nested_object_is_compact_json() {
750 let out = table_of(&json!({"id": 1, "author": {"name": "A"}}));
751 assert!(out.contains(r#"{"name":"A"}"#), "{out}");
752 }
753
754 #[test]
755 fn long_values_wrap_instead_of_widening_the_table() {
756 let long = "word ".repeat(80);
757 let out = table_of(&json!({"description": long}));
758 let widest = out.lines().map(|l| l.chars().count()).max().unwrap();
759 assert!(
760 widest < RECORD_VALUE_WIDTH + 30,
761 "widest line {widest}: {out}"
762 );
763 assert!(out.lines().count() > 4, "{out}");
764 }
765
766 #[test]
768 fn lists_and_bare_values_are_unchanged() {
769 let out = table_of(&json!([{"id": "1", "name": "Alice"}]));
770 assert!(out.contains("Alice") && !out.contains("field"), "{out}");
771 assert_eq!(table_of(&json!([])), "[]");
772 assert_eq!(table_of(&json!("text")), "\"text\"");
773 }
774
775 #[test]
776 fn human_formats_are_table_and_markdown_only() {
777 assert!(OutputFormat::Table.is_human());
778 assert!(OutputFormat::Markdown.is_human());
779 for f in [
780 OutputFormat::Json,
781 OutputFormat::Yaml,
782 OutputFormat::Csv,
783 OutputFormat::Quiet,
784 ] {
785 assert!(!f.is_human(), "{f:?}");
786 }
787 }
788
789 #[test]
790 fn test_coerce_rows_not_array() {
791 let value = json!({"id": "1", "name": "Alice"});
792 assert!(OutputRenderer::coerce_rows(&value).is_none());
793 }
794
795 #[test]
798 fn coerce_rows_without_columns_sorts_headers_alphabetically() {
799 let value = json!([{"zebra": "1", "apple": "2"}]);
800 let (headers, _) = OutputRenderer::coerce_rows(&value).unwrap();
801 assert_eq!(headers, vec!["apple".to_string(), "zebra".to_string()]);
802 }
803
804 #[test]
806 fn coerce_rows_with_columns_preserves_the_given_order() {
807 let value = json!([{"apple": "2", "zebra": "1"}]);
808 let columns = vec!["zebra".to_string(), "apple".to_string()];
809
810 let (headers, rows) = OutputRenderer::coerce_rows_with(&value, Some(&columns)).unwrap();
811
812 assert_eq!(headers, columns);
813 assert_eq!(rows[0], vec!["1".to_string(), "2".to_string()]);
814 }
815
816 #[test]
817 fn coerce_rows_with_columns_drops_keys_not_listed() {
818 let value = json!([{"wanted": "yes", "unwanted": "no"}]);
819 let columns = vec!["wanted".to_string()];
820
821 let (headers, rows) = OutputRenderer::coerce_rows_with(&value, Some(&columns)).unwrap();
822
823 assert_eq!(headers, columns);
824 assert_eq!(rows[0], vec!["yes".to_string()]);
825 }
826
827 #[test]
830 fn coerce_rows_with_columns_fills_absent_keys_with_empty() {
831 let value = json!([{"present": "here"}]);
832 let columns = vec!["present".to_string(), "absent".to_string()];
833
834 let (_, rows) = OutputRenderer::coerce_rows_with(&value, Some(&columns)).unwrap();
835
836 assert_eq!(rows[0], vec!["here".to_string(), String::new()]);
837 }
838
839 #[test]
840 fn coerce_rows_with_empty_columns_renders_nothing() {
841 let value = json!([{"id": "1"}]);
842 assert!(OutputRenderer::coerce_rows_with(&value, Some(&[])).is_none());
843 }
844
845 #[test]
846 fn test_coerce_rows_array_of_primitives() {
847 let value = json!(["one", "two", "three"]);
848 assert!(OutputRenderer::coerce_rows(&value).is_none());
849 }
850
851 #[test]
852 fn test_value_to_string_string() {
853 let value = json!("hello");
854 assert_eq!(OutputRenderer::value_to_string(&value), "hello");
855 }
856
857 #[test]
858 fn test_value_to_string_number() {
859 let value = json!(42);
860 assert_eq!(OutputRenderer::value_to_string(&value), "42");
861 }
862
863 #[test]
864 fn test_value_to_string_bool() {
865 let value = json!(true);
866 assert_eq!(OutputRenderer::value_to_string(&value), "true");
867 }
868
869 #[test]
870 fn test_value_to_string_null() {
871 let value = json!(null);
872 assert_eq!(OutputRenderer::value_to_string(&value), "");
873 }
874
875 #[test]
876 fn test_value_to_string_object() {
877 let value = json!({"key": "value"});
878 let result = OutputRenderer::value_to_string(&value);
879 assert!(result.contains("key"));
880 assert!(result.contains("value"));
881 }
882
883 #[test]
884 fn test_render_quiet_object_with_id() {
885 let value = json!({"id": "123", "name": "Test"});
886 let renderer = OutputRenderer::new(OutputFormat::Quiet);
887 assert!(renderer.render_quiet(&value));
888 }
889
890 #[test]
891 fn test_render_quiet_object_without_id() {
892 let value = json!({"name": "Test"});
893 let renderer = OutputRenderer::new(OutputFormat::Quiet);
894 assert!(!renderer.render_quiet(&value));
895 }
896
897 #[test]
898 fn test_render_quiet_array_with_ids() {
899 let value = json!([
900 {"id": "1", "name": "Alice"},
901 {"id": "2", "name": "Bob"}
902 ]);
903 let renderer = OutputRenderer::new(OutputFormat::Quiet);
904 assert!(renderer.render_quiet(&value));
905 }
906
907 #[test]
908 fn test_render_quiet_primitive() {
909 let value = json!("simple");
910 let renderer = OutputRenderer::new(OutputFormat::Quiet);
911 assert!(renderer.render_quiet(&value));
912 }
913
914 #[test]
915 fn test_render_quiet_null() {
916 let value = json!(null);
917 let renderer = OutputRenderer::new(OutputFormat::Quiet);
918 assert!(!renderer.render_quiet(&value));
919 }
920
921 #[test]
922 fn test_render_quiet_array_with_nulls() {
923 let value = json!([null, null]);
924 let renderer = OutputRenderer::new(OutputFormat::Quiet);
925 assert!(!renderer.render_quiet(&value));
926 }
927
928 #[derive(Serialize)]
929 struct TestStruct {
930 id: String,
931 name: String,
932 count: i32,
933 }
934
935 #[test]
936 fn test_render_json() {
937 let test_data = TestStruct {
938 id: "1".to_string(),
939 name: "Test".to_string(),
940 count: 42,
941 };
942
943 let renderer = OutputRenderer::new(OutputFormat::Json);
944 let result = renderer.render(&test_data);
945 assert!(result.is_ok());
946 }
947
948 #[test]
949 fn test_render_yaml() {
950 let test_data = TestStruct {
951 id: "1".to_string(),
952 name: "Test".to_string(),
953 count: 42,
954 };
955
956 let renderer = OutputRenderer::new(OutputFormat::Yaml);
957 let result = renderer.render(&test_data);
958 assert!(result.is_ok());
959 }
960
961 #[test]
962 fn test_render_table() {
963 let test_data = vec![
964 TestStruct {
965 id: "1".to_string(),
966 name: "Alice".to_string(),
967 count: 10,
968 },
969 TestStruct {
970 id: "2".to_string(),
971 name: "Bob".to_string(),
972 count: 20,
973 },
974 ];
975
976 let renderer = OutputRenderer::new(OutputFormat::Table);
977 let result = renderer.render(&test_data);
978 assert!(result.is_ok());
979 }
980
981 #[test]
982 fn test_render_csv() {
983 let test_data = vec![
984 TestStruct {
985 id: "1".to_string(),
986 name: "Alice".to_string(),
987 count: 10,
988 },
989 TestStruct {
990 id: "2".to_string(),
991 name: "Bob".to_string(),
992 count: 20,
993 },
994 ];
995
996 let renderer = OutputRenderer::new(OutputFormat::Csv);
997 let result = renderer.render(&test_data);
998 assert!(result.is_ok());
999 }
1000
1001 #[test]
1002 fn test_render_markdown_table() {
1003 let test_data = vec![
1004 TestStruct {
1005 id: "1".to_string(),
1006 name: "Alice".to_string(),
1007 count: 10,
1008 },
1009 TestStruct {
1010 id: "2".to_string(),
1011 name: "Bob".to_string(),
1012 count: 20,
1013 },
1014 ];
1015
1016 let renderer = OutputRenderer::new(OutputFormat::Markdown);
1017 let result = renderer.render(&test_data);
1018 assert!(result.is_ok());
1019 }
1020
1021 #[test]
1022 fn test_render_markdown_single_object() {
1023 let test_data = TestStruct {
1024 id: "1".to_string(),
1025 name: "Test".to_string(),
1026 count: 42,
1027 };
1028
1029 let renderer = OutputRenderer::new(OutputFormat::Markdown);
1030 let result = renderer.render(&test_data);
1031 assert!(result.is_ok());
1032 }
1033
1034 #[test]
1038 fn test_csv_field_quotes_per_rfc4180() {
1039 assert_eq!(OutputRenderer::csv_field("plain"), "plain");
1040 assert_eq!(OutputRenderer::csv_field("a,b"), "\"a,b\"");
1041 assert_eq!(
1042 OutputRenderer::csv_field("say \"hi\""),
1043 "\"say \"\"hi\"\"\""
1044 );
1045 assert_eq!(
1046 OutputRenderer::csv_field("line1\nline2"),
1047 "\"line1\nline2\""
1048 );
1049 assert_eq!(OutputRenderer::csv_field("cr\r"), "\"cr\r\"");
1050 assert_eq!(OutputRenderer::csv_field("no-specials"), "no-specials");
1052 }
1053
1054 #[test]
1055 fn test_csv_record_keeps_columns_aligned() {
1056 let fields = vec![
1057 "1".to_string(),
1058 "Fix bug, urgently".to_string(),
1059 "open".to_string(),
1060 ];
1061 assert_eq!(
1063 OutputRenderer::csv_record(&fields),
1064 "1,\"Fix bug, urgently\",open"
1065 );
1066 }
1067
1068 #[test]
1070 fn test_markdown_cell_escapes_newlines_and_pipes() {
1071 assert_eq!(OutputRenderer::markdown_cell("a|b"), "a\\|b");
1072 assert_eq!(OutputRenderer::markdown_cell("one\ntwo"), "one<br>two");
1073 assert_eq!(OutputRenderer::markdown_cell("one\r\ntwo"), "one<br>two");
1074 assert_eq!(OutputRenderer::markdown_cell("plain"), "plain");
1075 }
1076
1077 #[test]
1078 fn test_render_markdown_pipe_escaping() {
1079 let value = json!([
1080 {"col": "a|b", "val": "x|y"}
1081 ]);
1082 let renderer = OutputRenderer::new(OutputFormat::Markdown);
1083 assert!(renderer.render_markdown_table(&value).unwrap());
1085 }
1086
1087 #[test]
1088 fn test_render_raw() {
1089 let renderer = OutputRenderer::new(OutputFormat::Markdown);
1090 let result = renderer.render_raw("# Hello\n\nWorld");
1091 assert!(result.is_ok());
1092 }
1093
1094 #[test]
1095 fn test_render_list_without_envelope() {
1096 let data = vec![TestStruct {
1097 id: "1".to_string(),
1098 name: "Alice".to_string(),
1099 count: 10,
1100 }];
1101 let renderer = OutputRenderer::new(OutputFormat::Table);
1103 let result = renderer.render_list(&data);
1104 assert!(result.is_ok());
1105 }
1106
1107 #[test]
1108 fn test_render_list_with_envelope() {
1109 let data = vec![TestStruct {
1110 id: "1".to_string(),
1111 name: "Alice".to_string(),
1112 count: 10,
1113 }];
1114 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
1115 let result = renderer.render_list(&data);
1117 assert!(result.is_ok());
1118 }
1119
1120 #[test]
1121 fn test_render_list_empty_with_envelope() {
1122 let data: Vec<TestStruct> = vec![];
1123 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
1124 let result = renderer.render_list(&data);
1125 assert!(result.is_ok());
1126 }
1127
1128 #[test]
1129 fn test_with_envelope_setter() {
1130 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
1131 assert_eq!(renderer.format(), OutputFormat::Json);
1132 }
1133
1134 #[derive(Serialize)]
1139 struct EmptyRow {
1140 id: String,
1141 }
1142
1143 #[test]
1146 fn test_render_list_or_empty_json_emits_an_array() {
1147 let renderer = OutputRenderer::new(OutputFormat::Json);
1148 let rows: Vec<EmptyRow> = Vec::new();
1149 assert!(renderer
1152 .render_list_or_empty(&rows, "No rows found")
1153 .is_ok());
1154 }
1155
1156 #[test]
1157 fn test_render_list_or_empty_is_a_message_only_for_humans() {
1158 let rows: Vec<EmptyRow> = Vec::new();
1159 for format in [OutputFormat::Table, OutputFormat::Markdown] {
1160 let renderer = OutputRenderer::new(format);
1161 assert!(renderer
1162 .render_list_or_empty(&rows, "No rows found")
1163 .is_ok());
1164 }
1165 for format in [
1166 OutputFormat::Json,
1167 OutputFormat::Yaml,
1168 OutputFormat::Csv,
1169 OutputFormat::Quiet,
1170 ] {
1171 let renderer = OutputRenderer::new(format);
1172 assert!(renderer
1173 .render_list_or_empty(&rows, "No rows found")
1174 .is_ok());
1175 }
1176 }
1177
1178 #[test]
1180 fn test_render_list_or_empty_renders_rows_when_present() {
1181 let renderer = OutputRenderer::new(OutputFormat::Json);
1182 let rows = vec![EmptyRow {
1183 id: "1".to_string(),
1184 }];
1185 assert!(renderer
1186 .render_list_or_empty(&rows, "No rows found")
1187 .is_ok());
1188 }
1189
1190 #[test]
1193 fn test_render_list_or_empty_honours_the_envelope() {
1194 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
1195 let rows: Vec<EmptyRow> = Vec::new();
1196 assert!(renderer
1197 .render_list_or_empty(&rows, "No rows found")
1198 .is_ok());
1199 }
1200}