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 self.render(&items)
114 }
115
116 fn render_table(&self, value: &Value) -> Result<bool> {
117 let (headers, rows) = match Self::coerce_rows(value) {
118 Some(data) => data,
119 None => return Ok(false),
120 };
121
122 let mut builder = Builder::default();
123 builder.push_record(headers);
124 for row in rows {
125 builder.push_record(row);
126 }
127
128 let table = builder.build().with(Style::rounded()).to_string();
129 println!("{}", table);
130 Ok(true)
131 }
132
133 fn render_csv(&self, value: &Value) -> Result<bool> {
134 let (headers, rows) = match Self::coerce_rows(value) {
135 Some(data) => data,
136 None => return Ok(false),
137 };
138
139 println!("{}", Self::csv_record(&headers));
140 for row in rows {
141 println!("{}", Self::csv_record(&row));
142 }
143
144 Ok(true)
145 }
146
147 fn csv_record(fields: &[String]) -> String {
154 fields
155 .iter()
156 .map(|f| Self::csv_field(f))
157 .collect::<Vec<_>>()
158 .join(",")
159 }
160
161 fn csv_field(field: &str) -> String {
162 if field.contains([',', '"', '\n', '\r']) {
163 format!("\"{}\"", field.replace('"', "\"\""))
164 } else {
165 field.to_string()
166 }
167 }
168
169 fn render_quiet(&self, value: &Value) -> bool {
170 match value {
171 Value::Array(rows) => {
172 let mut printed = false;
173 for row in rows {
174 if let Value::Object(obj) = row {
175 if let Some(id) = obj.get("id").and_then(Value::as_str) {
176 println!("{id}");
177 printed = true;
178 } else if let Some(key) = obj.keys().next() {
179 if let Some(val) = obj.get(key) {
180 println!("{}", val);
181 printed = true;
182 }
183 }
184 } else if !row.is_null() {
185 println!("{}", row);
186 printed = true;
187 }
188 }
189 printed
190 }
191 Value::Object(obj) => {
192 if let Some(id) = obj.get("id").and_then(Value::as_str) {
193 println!("{id}");
194 true
195 } else {
196 false
197 }
198 }
199 Value::Null => false,
200 other => {
201 println!("{}", other);
202 true
203 }
204 }
205 }
206
207 pub fn render_raw(&self, content: &str) -> Result<()> {
209 println!("{content}");
210 Ok(())
211 }
212
213 fn render_markdown_table(&self, value: &Value) -> Result<bool> {
214 let (headers, rows) = match Self::coerce_rows(value) {
215 Some(data) => data,
216 None => return Ok(false),
217 };
218
219 let header_line: String = headers
221 .iter()
222 .map(|h| Self::markdown_cell(h))
223 .collect::<Vec<_>>()
224 .join(" | ");
225 println!("| {} |", header_line);
226
227 let separator: String = headers
229 .iter()
230 .map(|_| "---")
231 .collect::<Vec<_>>()
232 .join(" | ");
233 println!("| {} |", separator);
234
235 for row in rows {
237 let cells: String = row
238 .iter()
239 .map(|c| Self::markdown_cell(c))
240 .collect::<Vec<_>>()
241 .join(" | ");
242 println!("| {} |", cells);
243 }
244
245 Ok(true)
246 }
247
248 fn markdown_cell(cell: &str) -> String {
254 cell.replace('|', "\\|")
255 .replace("\r\n", "<br>")
256 .replace(['\n', '\r'], "<br>")
257 }
258
259 fn render_markdown_single(&self, value: &Value) -> Result<bool> {
260 if let Value::Object(obj) = value {
261 for (key, val) in obj {
262 let display = Self::value_to_string(val);
263 println!("**{}**: {}", key, display);
264 }
265 Ok(true)
266 } else {
267 println!("{}", serde_json::to_string_pretty(value)?);
268 Ok(true)
269 }
270 }
271
272 fn coerce_rows(value: &Value) -> Option<(Vec<String>, Vec<Vec<String>>)> {
273 let rows = match value {
274 Value::Array(rows) if !rows.is_empty() => rows,
275 _ => return None,
276 };
277
278 let mut headers = BTreeSet::new();
279 for row in rows {
280 if let Value::Object(obj) = row {
281 headers.extend(obj.keys().cloned());
282 }
283 }
284
285 if headers.is_empty() {
286 return None;
287 }
288
289 let headers_vec: Vec<String> = headers.into_iter().collect();
290 let mut data = Vec::with_capacity(rows.len());
291 for row in rows {
292 let mut record = Vec::with_capacity(headers_vec.len());
293 if let Value::Object(obj) = row {
294 for header in &headers_vec {
295 let cell = obj
296 .get(header)
297 .map(Self::value_to_string)
298 .unwrap_or_else(|| "".to_string());
299 record.push(cell);
300 }
301 }
302 data.push(record);
303 }
304
305 Some((headers_vec, data))
306 }
307
308 fn value_to_string(value: &Value) -> String {
309 match value {
310 Value::String(s) => s.clone(),
311 Value::Number(n) => n.to_string(),
312 Value::Bool(b) => b.to_string(),
313 Value::Null => String::new(),
314 other => serde_json::to_string(other).unwrap_or_default(),
315 }
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use serde_json::json;
323
324 #[test]
325 fn test_output_format_default() {
326 assert_eq!(OutputFormat::default(), OutputFormat::Table);
327 }
328
329 #[test]
330 fn test_renderer_new() {
331 let renderer = OutputRenderer::new(OutputFormat::Json);
332 assert_eq!(renderer.format(), OutputFormat::Json);
333 }
334
335 #[test]
336 fn test_coerce_rows_empty_array() {
337 let value = json!([]);
338 assert!(OutputRenderer::coerce_rows(&value).is_none());
339 }
340
341 #[test]
342 fn test_coerce_rows_single_object() {
343 let value = json!([
344 {"id": "1", "name": "Alice"},
345 {"id": "2", "name": "Bob"}
346 ]);
347
348 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
349 assert_eq!(headers.len(), 2);
350 assert!(headers.contains(&"id".to_string()));
351 assert!(headers.contains(&"name".to_string()));
352 assert_eq!(rows.len(), 2);
353 }
354
355 #[test]
356 fn test_coerce_rows_mixed_keys() {
357 let value = json!([
358 {"id": "1", "name": "Alice"},
359 {"id": "2", "email": "bob@example.com"}
360 ]);
361
362 let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
363 assert_eq!(headers.len(), 3);
364 assert!(headers.contains(&"id".to_string()));
365 assert!(headers.contains(&"name".to_string()));
366 assert!(headers.contains(&"email".to_string()));
367
368 assert_eq!(
369 rows[0][headers.iter().position(|h| h == "id").unwrap()],
370 "1"
371 );
372 assert_eq!(
373 rows[0][headers.iter().position(|h| h == "name").unwrap()],
374 "Alice"
375 );
376 assert_eq!(
377 rows[0][headers.iter().position(|h| h == "email").unwrap()],
378 ""
379 );
380 }
381
382 #[test]
383 fn test_coerce_rows_not_array() {
384 let value = json!({"id": "1", "name": "Alice"});
385 assert!(OutputRenderer::coerce_rows(&value).is_none());
386 }
387
388 #[test]
389 fn test_coerce_rows_array_of_primitives() {
390 let value = json!(["one", "two", "three"]);
391 assert!(OutputRenderer::coerce_rows(&value).is_none());
392 }
393
394 #[test]
395 fn test_value_to_string_string() {
396 let value = json!("hello");
397 assert_eq!(OutputRenderer::value_to_string(&value), "hello");
398 }
399
400 #[test]
401 fn test_value_to_string_number() {
402 let value = json!(42);
403 assert_eq!(OutputRenderer::value_to_string(&value), "42");
404 }
405
406 #[test]
407 fn test_value_to_string_bool() {
408 let value = json!(true);
409 assert_eq!(OutputRenderer::value_to_string(&value), "true");
410 }
411
412 #[test]
413 fn test_value_to_string_null() {
414 let value = json!(null);
415 assert_eq!(OutputRenderer::value_to_string(&value), "");
416 }
417
418 #[test]
419 fn test_value_to_string_object() {
420 let value = json!({"key": "value"});
421 let result = OutputRenderer::value_to_string(&value);
422 assert!(result.contains("key"));
423 assert!(result.contains("value"));
424 }
425
426 #[test]
427 fn test_render_quiet_object_with_id() {
428 let value = json!({"id": "123", "name": "Test"});
429 let renderer = OutputRenderer::new(OutputFormat::Quiet);
430 assert!(renderer.render_quiet(&value));
431 }
432
433 #[test]
434 fn test_render_quiet_object_without_id() {
435 let value = json!({"name": "Test"});
436 let renderer = OutputRenderer::new(OutputFormat::Quiet);
437 assert!(!renderer.render_quiet(&value));
438 }
439
440 #[test]
441 fn test_render_quiet_array_with_ids() {
442 let value = json!([
443 {"id": "1", "name": "Alice"},
444 {"id": "2", "name": "Bob"}
445 ]);
446 let renderer = OutputRenderer::new(OutputFormat::Quiet);
447 assert!(renderer.render_quiet(&value));
448 }
449
450 #[test]
451 fn test_render_quiet_primitive() {
452 let value = json!("simple");
453 let renderer = OutputRenderer::new(OutputFormat::Quiet);
454 assert!(renderer.render_quiet(&value));
455 }
456
457 #[test]
458 fn test_render_quiet_null() {
459 let value = json!(null);
460 let renderer = OutputRenderer::new(OutputFormat::Quiet);
461 assert!(!renderer.render_quiet(&value));
462 }
463
464 #[test]
465 fn test_render_quiet_array_with_nulls() {
466 let value = json!([null, null]);
467 let renderer = OutputRenderer::new(OutputFormat::Quiet);
468 assert!(!renderer.render_quiet(&value));
469 }
470
471 #[derive(Serialize)]
472 struct TestStruct {
473 id: String,
474 name: String,
475 count: i32,
476 }
477
478 #[test]
479 fn test_render_json() {
480 let test_data = TestStruct {
481 id: "1".to_string(),
482 name: "Test".to_string(),
483 count: 42,
484 };
485
486 let renderer = OutputRenderer::new(OutputFormat::Json);
487 let result = renderer.render(&test_data);
488 assert!(result.is_ok());
489 }
490
491 #[test]
492 fn test_render_yaml() {
493 let test_data = TestStruct {
494 id: "1".to_string(),
495 name: "Test".to_string(),
496 count: 42,
497 };
498
499 let renderer = OutputRenderer::new(OutputFormat::Yaml);
500 let result = renderer.render(&test_data);
501 assert!(result.is_ok());
502 }
503
504 #[test]
505 fn test_render_table() {
506 let test_data = vec![
507 TestStruct {
508 id: "1".to_string(),
509 name: "Alice".to_string(),
510 count: 10,
511 },
512 TestStruct {
513 id: "2".to_string(),
514 name: "Bob".to_string(),
515 count: 20,
516 },
517 ];
518
519 let renderer = OutputRenderer::new(OutputFormat::Table);
520 let result = renderer.render(&test_data);
521 assert!(result.is_ok());
522 }
523
524 #[test]
525 fn test_render_csv() {
526 let test_data = vec![
527 TestStruct {
528 id: "1".to_string(),
529 name: "Alice".to_string(),
530 count: 10,
531 },
532 TestStruct {
533 id: "2".to_string(),
534 name: "Bob".to_string(),
535 count: 20,
536 },
537 ];
538
539 let renderer = OutputRenderer::new(OutputFormat::Csv);
540 let result = renderer.render(&test_data);
541 assert!(result.is_ok());
542 }
543
544 #[test]
545 fn test_render_markdown_table() {
546 let test_data = vec![
547 TestStruct {
548 id: "1".to_string(),
549 name: "Alice".to_string(),
550 count: 10,
551 },
552 TestStruct {
553 id: "2".to_string(),
554 name: "Bob".to_string(),
555 count: 20,
556 },
557 ];
558
559 let renderer = OutputRenderer::new(OutputFormat::Markdown);
560 let result = renderer.render(&test_data);
561 assert!(result.is_ok());
562 }
563
564 #[test]
565 fn test_render_markdown_single_object() {
566 let test_data = TestStruct {
567 id: "1".to_string(),
568 name: "Test".to_string(),
569 count: 42,
570 };
571
572 let renderer = OutputRenderer::new(OutputFormat::Markdown);
573 let result = renderer.render(&test_data);
574 assert!(result.is_ok());
575 }
576
577 #[test]
581 fn test_csv_field_quotes_per_rfc4180() {
582 assert_eq!(OutputRenderer::csv_field("plain"), "plain");
583 assert_eq!(OutputRenderer::csv_field("a,b"), "\"a,b\"");
584 assert_eq!(
585 OutputRenderer::csv_field("say \"hi\""),
586 "\"say \"\"hi\"\"\""
587 );
588 assert_eq!(
589 OutputRenderer::csv_field("line1\nline2"),
590 "\"line1\nline2\""
591 );
592 assert_eq!(OutputRenderer::csv_field("cr\r"), "\"cr\r\"");
593 assert_eq!(OutputRenderer::csv_field("no-specials"), "no-specials");
595 }
596
597 #[test]
598 fn test_csv_record_keeps_columns_aligned() {
599 let fields = vec![
600 "1".to_string(),
601 "Fix bug, urgently".to_string(),
602 "open".to_string(),
603 ];
604 assert_eq!(
606 OutputRenderer::csv_record(&fields),
607 "1,\"Fix bug, urgently\",open"
608 );
609 }
610
611 #[test]
613 fn test_markdown_cell_escapes_newlines_and_pipes() {
614 assert_eq!(OutputRenderer::markdown_cell("a|b"), "a\\|b");
615 assert_eq!(OutputRenderer::markdown_cell("one\ntwo"), "one<br>two");
616 assert_eq!(OutputRenderer::markdown_cell("one\r\ntwo"), "one<br>two");
617 assert_eq!(OutputRenderer::markdown_cell("plain"), "plain");
618 }
619
620 #[test]
621 fn test_render_markdown_pipe_escaping() {
622 let value = json!([
623 {"col": "a|b", "val": "x|y"}
624 ]);
625 let renderer = OutputRenderer::new(OutputFormat::Markdown);
626 assert!(renderer.render_markdown_table(&value).unwrap());
628 }
629
630 #[test]
631 fn test_render_raw() {
632 let renderer = OutputRenderer::new(OutputFormat::Markdown);
633 let result = renderer.render_raw("# Hello\n\nWorld");
634 assert!(result.is_ok());
635 }
636
637 #[test]
638 fn test_render_list_without_envelope() {
639 let data = vec![TestStruct {
640 id: "1".to_string(),
641 name: "Alice".to_string(),
642 count: 10,
643 }];
644 let renderer = OutputRenderer::new(OutputFormat::Table);
646 let result = renderer.render_list(&data);
647 assert!(result.is_ok());
648 }
649
650 #[test]
651 fn test_render_list_with_envelope() {
652 let data = vec![TestStruct {
653 id: "1".to_string(),
654 name: "Alice".to_string(),
655 count: 10,
656 }];
657 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
658 let result = renderer.render_list(&data);
660 assert!(result.is_ok());
661 }
662
663 #[test]
664 fn test_render_list_empty_with_envelope() {
665 let data: Vec<TestStruct> = vec![];
666 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
667 let result = renderer.render_list(&data);
668 assert!(result.is_ok());
669 }
670
671 #[test]
672 fn test_with_envelope_setter() {
673 let renderer = OutputRenderer::new(OutputFormat::Json).with_envelope(true);
674 assert_eq!(renderer.format(), OutputFormat::Json);
675 }
676}