Skip to main content

atlassian_cli_output/
lib.rs

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}
28
29impl OutputRenderer {
30    pub fn new(format: OutputFormat) -> Self {
31        Self { format }
32    }
33
34    pub fn format(&self) -> OutputFormat {
35        self.format
36    }
37
38    pub fn render<T: Serialize>(&self, value: &T) -> Result<()> {
39        let json_value = serde_json::to_value(value)?;
40
41        match self.format {
42            OutputFormat::Table => {
43                if !self.render_table(&json_value)? {
44                    println!("{}", serde_json::to_string_pretty(&json_value)?);
45                }
46            }
47            OutputFormat::Json => {
48                println!("{}", serde_json::to_string_pretty(&json_value)?);
49            }
50            OutputFormat::Yaml => {
51                println!("{}", serde_yaml::to_string(&json_value)?);
52            }
53            OutputFormat::Csv => {
54                if !self.render_csv(&json_value)? {
55                    println!("{}", serde_json::to_string_pretty(&json_value)?);
56                }
57            }
58            OutputFormat::Quiet => {
59                if !self.render_quiet(&json_value) {
60                    println!("{}", serde_json::to_string_pretty(&json_value)?);
61                }
62            }
63            OutputFormat::Markdown => {
64                if !self.render_markdown_table(&json_value)? {
65                    self.render_markdown_single(&json_value)?;
66                }
67            }
68        }
69
70        Ok(())
71    }
72
73    fn render_table(&self, value: &Value) -> Result<bool> {
74        let (headers, rows) = match Self::coerce_rows(value) {
75            Some(data) => data,
76            None => return Ok(false),
77        };
78
79        let mut builder = Builder::default();
80        builder.push_record(headers);
81        for row in rows {
82            builder.push_record(row);
83        }
84
85        let table = builder.build().with(Style::rounded()).to_string();
86        println!("{}", table);
87        Ok(true)
88    }
89
90    fn render_csv(&self, value: &Value) -> Result<bool> {
91        let (headers, rows) = match Self::coerce_rows(value) {
92            Some(data) => data,
93            None => return Ok(false),
94        };
95
96        println!("{}", headers.join(","));
97        for row in rows {
98            println!("{}", row.join(","));
99        }
100
101        Ok(true)
102    }
103
104    fn render_quiet(&self, value: &Value) -> bool {
105        match value {
106            Value::Array(rows) => {
107                let mut printed = false;
108                for row in rows {
109                    if let Value::Object(obj) = row {
110                        if let Some(id) = obj.get("id").and_then(Value::as_str) {
111                            println!("{id}");
112                            printed = true;
113                        } else if let Some(key) = obj.keys().next() {
114                            if let Some(val) = obj.get(key) {
115                                println!("{}", val);
116                                printed = true;
117                            }
118                        }
119                    } else if !row.is_null() {
120                        println!("{}", row);
121                        printed = true;
122                    }
123                }
124                printed
125            }
126            Value::Object(obj) => {
127                if let Some(id) = obj.get("id").and_then(Value::as_str) {
128                    println!("{id}");
129                    true
130                } else {
131                    false
132                }
133            }
134            Value::Null => false,
135            other => {
136                println!("{}", other);
137                true
138            }
139        }
140    }
141
142    /// Render pre-formatted content directly to stdout (e.g. for markdown issue views).
143    pub fn render_raw(&self, content: &str) -> Result<()> {
144        println!("{content}");
145        Ok(())
146    }
147
148    fn render_markdown_table(&self, value: &Value) -> Result<bool> {
149        let (headers, rows) = match Self::coerce_rows(value) {
150            Some(data) => data,
151            None => return Ok(false),
152        };
153
154        // Header row
155        let header_line: String = headers
156            .iter()
157            .map(|h| h.replace('|', "\\|"))
158            .collect::<Vec<_>>()
159            .join(" | ");
160        println!("| {} |", header_line);
161
162        // Separator row
163        let separator: String = headers
164            .iter()
165            .map(|_| "---")
166            .collect::<Vec<_>>()
167            .join(" | ");
168        println!("| {} |", separator);
169
170        // Data rows
171        for row in rows {
172            let cells: String = row
173                .iter()
174                .map(|c| c.replace('|', "\\|"))
175                .collect::<Vec<_>>()
176                .join(" | ");
177            println!("| {} |", cells);
178        }
179
180        Ok(true)
181    }
182
183    fn render_markdown_single(&self, value: &Value) -> Result<bool> {
184        if let Value::Object(obj) = value {
185            for (key, val) in obj {
186                let display = Self::value_to_string(val);
187                println!("**{}**: {}", key, display);
188            }
189            Ok(true)
190        } else {
191            println!("{}", serde_json::to_string_pretty(value)?);
192            Ok(true)
193        }
194    }
195
196    fn coerce_rows(value: &Value) -> Option<(Vec<String>, Vec<Vec<String>>)> {
197        let rows = match value {
198            Value::Array(rows) if !rows.is_empty() => rows,
199            _ => return None,
200        };
201
202        let mut headers = BTreeSet::new();
203        for row in rows {
204            if let Value::Object(obj) = row {
205                headers.extend(obj.keys().cloned());
206            }
207        }
208
209        if headers.is_empty() {
210            return None;
211        }
212
213        let headers_vec: Vec<String> = headers.into_iter().collect();
214        let mut data = Vec::with_capacity(rows.len());
215        for row in rows {
216            let mut record = Vec::with_capacity(headers_vec.len());
217            if let Value::Object(obj) = row {
218                for header in &headers_vec {
219                    let cell = obj
220                        .get(header)
221                        .map(Self::value_to_string)
222                        .unwrap_or_else(|| "".to_string());
223                    record.push(cell);
224                }
225            }
226            data.push(record);
227        }
228
229        Some((headers_vec, data))
230    }
231
232    fn value_to_string(value: &Value) -> String {
233        match value {
234            Value::String(s) => s.clone(),
235            Value::Number(n) => n.to_string(),
236            Value::Bool(b) => b.to_string(),
237            Value::Null => String::new(),
238            other => serde_json::to_string(other).unwrap_or_default(),
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use serde_json::json;
247
248    #[test]
249    fn test_output_format_default() {
250        assert_eq!(OutputFormat::default(), OutputFormat::Table);
251    }
252
253    #[test]
254    fn test_renderer_new() {
255        let renderer = OutputRenderer::new(OutputFormat::Json);
256        assert_eq!(renderer.format(), OutputFormat::Json);
257    }
258
259    #[test]
260    fn test_coerce_rows_empty_array() {
261        let value = json!([]);
262        assert!(OutputRenderer::coerce_rows(&value).is_none());
263    }
264
265    #[test]
266    fn test_coerce_rows_single_object() {
267        let value = json!([
268            {"id": "1", "name": "Alice"},
269            {"id": "2", "name": "Bob"}
270        ]);
271
272        let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
273        assert_eq!(headers.len(), 2);
274        assert!(headers.contains(&"id".to_string()));
275        assert!(headers.contains(&"name".to_string()));
276        assert_eq!(rows.len(), 2);
277    }
278
279    #[test]
280    fn test_coerce_rows_mixed_keys() {
281        let value = json!([
282            {"id": "1", "name": "Alice"},
283            {"id": "2", "email": "bob@example.com"}
284        ]);
285
286        let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
287        assert_eq!(headers.len(), 3);
288        assert!(headers.contains(&"id".to_string()));
289        assert!(headers.contains(&"name".to_string()));
290        assert!(headers.contains(&"email".to_string()));
291
292        assert_eq!(
293            rows[0][headers.iter().position(|h| h == "id").unwrap()],
294            "1"
295        );
296        assert_eq!(
297            rows[0][headers.iter().position(|h| h == "name").unwrap()],
298            "Alice"
299        );
300        assert_eq!(
301            rows[0][headers.iter().position(|h| h == "email").unwrap()],
302            ""
303        );
304    }
305
306    #[test]
307    fn test_coerce_rows_not_array() {
308        let value = json!({"id": "1", "name": "Alice"});
309        assert!(OutputRenderer::coerce_rows(&value).is_none());
310    }
311
312    #[test]
313    fn test_coerce_rows_array_of_primitives() {
314        let value = json!(["one", "two", "three"]);
315        assert!(OutputRenderer::coerce_rows(&value).is_none());
316    }
317
318    #[test]
319    fn test_value_to_string_string() {
320        let value = json!("hello");
321        assert_eq!(OutputRenderer::value_to_string(&value), "hello");
322    }
323
324    #[test]
325    fn test_value_to_string_number() {
326        let value = json!(42);
327        assert_eq!(OutputRenderer::value_to_string(&value), "42");
328    }
329
330    #[test]
331    fn test_value_to_string_bool() {
332        let value = json!(true);
333        assert_eq!(OutputRenderer::value_to_string(&value), "true");
334    }
335
336    #[test]
337    fn test_value_to_string_null() {
338        let value = json!(null);
339        assert_eq!(OutputRenderer::value_to_string(&value), "");
340    }
341
342    #[test]
343    fn test_value_to_string_object() {
344        let value = json!({"key": "value"});
345        let result = OutputRenderer::value_to_string(&value);
346        assert!(result.contains("key"));
347        assert!(result.contains("value"));
348    }
349
350    #[test]
351    fn test_render_quiet_object_with_id() {
352        let value = json!({"id": "123", "name": "Test"});
353        let renderer = OutputRenderer::new(OutputFormat::Quiet);
354        assert!(renderer.render_quiet(&value));
355    }
356
357    #[test]
358    fn test_render_quiet_object_without_id() {
359        let value = json!({"name": "Test"});
360        let renderer = OutputRenderer::new(OutputFormat::Quiet);
361        assert!(!renderer.render_quiet(&value));
362    }
363
364    #[test]
365    fn test_render_quiet_array_with_ids() {
366        let value = json!([
367            {"id": "1", "name": "Alice"},
368            {"id": "2", "name": "Bob"}
369        ]);
370        let renderer = OutputRenderer::new(OutputFormat::Quiet);
371        assert!(renderer.render_quiet(&value));
372    }
373
374    #[test]
375    fn test_render_quiet_primitive() {
376        let value = json!("simple");
377        let renderer = OutputRenderer::new(OutputFormat::Quiet);
378        assert!(renderer.render_quiet(&value));
379    }
380
381    #[test]
382    fn test_render_quiet_null() {
383        let value = json!(null);
384        let renderer = OutputRenderer::new(OutputFormat::Quiet);
385        assert!(!renderer.render_quiet(&value));
386    }
387
388    #[test]
389    fn test_render_quiet_array_with_nulls() {
390        let value = json!([null, null]);
391        let renderer = OutputRenderer::new(OutputFormat::Quiet);
392        assert!(!renderer.render_quiet(&value));
393    }
394
395    #[derive(Serialize)]
396    struct TestStruct {
397        id: String,
398        name: String,
399        count: i32,
400    }
401
402    #[test]
403    fn test_render_json() {
404        let test_data = TestStruct {
405            id: "1".to_string(),
406            name: "Test".to_string(),
407            count: 42,
408        };
409
410        let renderer = OutputRenderer::new(OutputFormat::Json);
411        let result = renderer.render(&test_data);
412        assert!(result.is_ok());
413    }
414
415    #[test]
416    fn test_render_yaml() {
417        let test_data = TestStruct {
418            id: "1".to_string(),
419            name: "Test".to_string(),
420            count: 42,
421        };
422
423        let renderer = OutputRenderer::new(OutputFormat::Yaml);
424        let result = renderer.render(&test_data);
425        assert!(result.is_ok());
426    }
427
428    #[test]
429    fn test_render_table() {
430        let test_data = vec![
431            TestStruct {
432                id: "1".to_string(),
433                name: "Alice".to_string(),
434                count: 10,
435            },
436            TestStruct {
437                id: "2".to_string(),
438                name: "Bob".to_string(),
439                count: 20,
440            },
441        ];
442
443        let renderer = OutputRenderer::new(OutputFormat::Table);
444        let result = renderer.render(&test_data);
445        assert!(result.is_ok());
446    }
447
448    #[test]
449    fn test_render_csv() {
450        let test_data = vec![
451            TestStruct {
452                id: "1".to_string(),
453                name: "Alice".to_string(),
454                count: 10,
455            },
456            TestStruct {
457                id: "2".to_string(),
458                name: "Bob".to_string(),
459                count: 20,
460            },
461        ];
462
463        let renderer = OutputRenderer::new(OutputFormat::Csv);
464        let result = renderer.render(&test_data);
465        assert!(result.is_ok());
466    }
467
468    #[test]
469    fn test_render_markdown_table() {
470        let test_data = vec![
471            TestStruct {
472                id: "1".to_string(),
473                name: "Alice".to_string(),
474                count: 10,
475            },
476            TestStruct {
477                id: "2".to_string(),
478                name: "Bob".to_string(),
479                count: 20,
480            },
481        ];
482
483        let renderer = OutputRenderer::new(OutputFormat::Markdown);
484        let result = renderer.render(&test_data);
485        assert!(result.is_ok());
486    }
487
488    #[test]
489    fn test_render_markdown_single_object() {
490        let test_data = TestStruct {
491            id: "1".to_string(),
492            name: "Test".to_string(),
493            count: 42,
494        };
495
496        let renderer = OutputRenderer::new(OutputFormat::Markdown);
497        let result = renderer.render(&test_data);
498        assert!(result.is_ok());
499    }
500
501    #[test]
502    fn test_render_markdown_pipe_escaping() {
503        let value = json!([
504            {"col": "a|b", "val": "x|y"}
505        ]);
506        let renderer = OutputRenderer::new(OutputFormat::Markdown);
507        // Should not panic; pipes in values should be escaped
508        assert!(renderer.render_markdown_table(&value).unwrap());
509    }
510
511    #[test]
512    fn test_render_raw() {
513        let renderer = OutputRenderer::new(OutputFormat::Markdown);
514        let result = renderer.render_raw("# Hello\n\nWorld");
515        assert!(result.is_ok());
516    }
517}