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