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}
23
24pub struct OutputRenderer {
25    format: OutputFormat,
26}
27
28impl OutputRenderer {
29    pub fn new(format: OutputFormat) -> Self {
30        Self { format }
31    }
32
33    pub fn format(&self) -> OutputFormat {
34        self.format
35    }
36
37    pub fn render<T: Serialize>(&self, value: &T) -> Result<()> {
38        let json_value = serde_json::to_value(value)?;
39
40        match self.format {
41            OutputFormat::Table => {
42                if !self.render_table(&json_value)? {
43                    println!("{}", serde_json::to_string_pretty(&json_value)?);
44                }
45            }
46            OutputFormat::Json => {
47                println!("{}", serde_json::to_string_pretty(&json_value)?);
48            }
49            OutputFormat::Yaml => {
50                println!("{}", serde_yaml::to_string(&json_value)?);
51            }
52            OutputFormat::Csv => {
53                if !self.render_csv(&json_value)? {
54                    println!("{}", serde_json::to_string_pretty(&json_value)?);
55                }
56            }
57            OutputFormat::Quiet => {
58                if !self.render_quiet(&json_value) {
59                    println!("{}", serde_json::to_string_pretty(&json_value)?);
60                }
61            }
62        }
63
64        Ok(())
65    }
66
67    fn render_table(&self, value: &Value) -> Result<bool> {
68        let (headers, rows) = match Self::coerce_rows(value) {
69            Some(data) => data,
70            None => return Ok(false),
71        };
72
73        let mut builder = Builder::default();
74        builder.push_record(headers);
75        for row in rows {
76            builder.push_record(row);
77        }
78
79        let table = builder.build().with(Style::rounded()).to_string();
80        println!("{}", table);
81        Ok(true)
82    }
83
84    fn render_csv(&self, value: &Value) -> Result<bool> {
85        let (headers, rows) = match Self::coerce_rows(value) {
86            Some(data) => data,
87            None => return Ok(false),
88        };
89
90        println!("{}", headers.join(","));
91        for row in rows {
92            println!("{}", row.join(","));
93        }
94
95        Ok(true)
96    }
97
98    fn render_quiet(&self, value: &Value) -> bool {
99        match value {
100            Value::Array(rows) => {
101                let mut printed = false;
102                for row in rows {
103                    if let Value::Object(obj) = row {
104                        if let Some(id) = obj.get("id").and_then(Value::as_str) {
105                            println!("{id}");
106                            printed = true;
107                        } else if let Some(key) = obj.keys().next() {
108                            if let Some(val) = obj.get(key) {
109                                println!("{}", val);
110                                printed = true;
111                            }
112                        }
113                    } else if !row.is_null() {
114                        println!("{}", row);
115                        printed = true;
116                    }
117                }
118                printed
119            }
120            Value::Object(obj) => {
121                if let Some(id) = obj.get("id").and_then(Value::as_str) {
122                    println!("{id}");
123                    true
124                } else {
125                    false
126                }
127            }
128            Value::Null => false,
129            other => {
130                println!("{}", other);
131                true
132            }
133        }
134    }
135
136    fn coerce_rows(value: &Value) -> Option<(Vec<String>, Vec<Vec<String>>)> {
137        let rows = match value {
138            Value::Array(rows) if !rows.is_empty() => rows,
139            _ => return None,
140        };
141
142        let mut headers = BTreeSet::new();
143        for row in rows {
144            if let Value::Object(obj) = row {
145                headers.extend(obj.keys().cloned());
146            }
147        }
148
149        if headers.is_empty() {
150            return None;
151        }
152
153        let headers_vec: Vec<String> = headers.into_iter().collect();
154        let mut data = Vec::with_capacity(rows.len());
155        for row in rows {
156            let mut record = Vec::with_capacity(headers_vec.len());
157            if let Value::Object(obj) = row {
158                for header in &headers_vec {
159                    let cell = obj
160                        .get(header)
161                        .map(Self::value_to_string)
162                        .unwrap_or_else(|| "".to_string());
163                    record.push(cell);
164                }
165            }
166            data.push(record);
167        }
168
169        Some((headers_vec, data))
170    }
171
172    fn value_to_string(value: &Value) -> String {
173        match value {
174            Value::String(s) => s.clone(),
175            Value::Number(n) => n.to_string(),
176            Value::Bool(b) => b.to_string(),
177            Value::Null => String::new(),
178            other => serde_json::to_string(other).unwrap_or_default(),
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use serde_json::json;
187
188    #[test]
189    fn test_output_format_default() {
190        assert_eq!(OutputFormat::default(), OutputFormat::Table);
191    }
192
193    #[test]
194    fn test_renderer_new() {
195        let renderer = OutputRenderer::new(OutputFormat::Json);
196        assert_eq!(renderer.format(), OutputFormat::Json);
197    }
198
199    #[test]
200    fn test_coerce_rows_empty_array() {
201        let value = json!([]);
202        assert!(OutputRenderer::coerce_rows(&value).is_none());
203    }
204
205    #[test]
206    fn test_coerce_rows_single_object() {
207        let value = json!([
208            {"id": "1", "name": "Alice"},
209            {"id": "2", "name": "Bob"}
210        ]);
211
212        let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
213        assert_eq!(headers.len(), 2);
214        assert!(headers.contains(&"id".to_string()));
215        assert!(headers.contains(&"name".to_string()));
216        assert_eq!(rows.len(), 2);
217    }
218
219    #[test]
220    fn test_coerce_rows_mixed_keys() {
221        let value = json!([
222            {"id": "1", "name": "Alice"},
223            {"id": "2", "email": "bob@example.com"}
224        ]);
225
226        let (headers, rows) = OutputRenderer::coerce_rows(&value).unwrap();
227        assert_eq!(headers.len(), 3);
228        assert!(headers.contains(&"id".to_string()));
229        assert!(headers.contains(&"name".to_string()));
230        assert!(headers.contains(&"email".to_string()));
231
232        assert_eq!(
233            rows[0][headers.iter().position(|h| h == "id").unwrap()],
234            "1"
235        );
236        assert_eq!(
237            rows[0][headers.iter().position(|h| h == "name").unwrap()],
238            "Alice"
239        );
240        assert_eq!(
241            rows[0][headers.iter().position(|h| h == "email").unwrap()],
242            ""
243        );
244    }
245
246    #[test]
247    fn test_coerce_rows_not_array() {
248        let value = json!({"id": "1", "name": "Alice"});
249        assert!(OutputRenderer::coerce_rows(&value).is_none());
250    }
251
252    #[test]
253    fn test_coerce_rows_array_of_primitives() {
254        let value = json!(["one", "two", "three"]);
255        assert!(OutputRenderer::coerce_rows(&value).is_none());
256    }
257
258    #[test]
259    fn test_value_to_string_string() {
260        let value = json!("hello");
261        assert_eq!(OutputRenderer::value_to_string(&value), "hello");
262    }
263
264    #[test]
265    fn test_value_to_string_number() {
266        let value = json!(42);
267        assert_eq!(OutputRenderer::value_to_string(&value), "42");
268    }
269
270    #[test]
271    fn test_value_to_string_bool() {
272        let value = json!(true);
273        assert_eq!(OutputRenderer::value_to_string(&value), "true");
274    }
275
276    #[test]
277    fn test_value_to_string_null() {
278        let value = json!(null);
279        assert_eq!(OutputRenderer::value_to_string(&value), "");
280    }
281
282    #[test]
283    fn test_value_to_string_object() {
284        let value = json!({"key": "value"});
285        let result = OutputRenderer::value_to_string(&value);
286        assert!(result.contains("key"));
287        assert!(result.contains("value"));
288    }
289
290    #[test]
291    fn test_render_quiet_object_with_id() {
292        let value = json!({"id": "123", "name": "Test"});
293        let renderer = OutputRenderer::new(OutputFormat::Quiet);
294        assert!(renderer.render_quiet(&value));
295    }
296
297    #[test]
298    fn test_render_quiet_object_without_id() {
299        let value = json!({"name": "Test"});
300        let renderer = OutputRenderer::new(OutputFormat::Quiet);
301        assert!(!renderer.render_quiet(&value));
302    }
303
304    #[test]
305    fn test_render_quiet_array_with_ids() {
306        let value = json!([
307            {"id": "1", "name": "Alice"},
308            {"id": "2", "name": "Bob"}
309        ]);
310        let renderer = OutputRenderer::new(OutputFormat::Quiet);
311        assert!(renderer.render_quiet(&value));
312    }
313
314    #[test]
315    fn test_render_quiet_primitive() {
316        let value = json!("simple");
317        let renderer = OutputRenderer::new(OutputFormat::Quiet);
318        assert!(renderer.render_quiet(&value));
319    }
320
321    #[test]
322    fn test_render_quiet_null() {
323        let value = json!(null);
324        let renderer = OutputRenderer::new(OutputFormat::Quiet);
325        assert!(!renderer.render_quiet(&value));
326    }
327
328    #[test]
329    fn test_render_quiet_array_with_nulls() {
330        let value = json!([null, null]);
331        let renderer = OutputRenderer::new(OutputFormat::Quiet);
332        assert!(!renderer.render_quiet(&value));
333    }
334
335    #[derive(Serialize)]
336    struct TestStruct {
337        id: String,
338        name: String,
339        count: i32,
340    }
341
342    #[test]
343    fn test_render_json() {
344        let test_data = TestStruct {
345            id: "1".to_string(),
346            name: "Test".to_string(),
347            count: 42,
348        };
349
350        let renderer = OutputRenderer::new(OutputFormat::Json);
351        let result = renderer.render(&test_data);
352        assert!(result.is_ok());
353    }
354
355    #[test]
356    fn test_render_yaml() {
357        let test_data = TestStruct {
358            id: "1".to_string(),
359            name: "Test".to_string(),
360            count: 42,
361        };
362
363        let renderer = OutputRenderer::new(OutputFormat::Yaml);
364        let result = renderer.render(&test_data);
365        assert!(result.is_ok());
366    }
367
368    #[test]
369    fn test_render_table() {
370        let test_data = vec![
371            TestStruct {
372                id: "1".to_string(),
373                name: "Alice".to_string(),
374                count: 10,
375            },
376            TestStruct {
377                id: "2".to_string(),
378                name: "Bob".to_string(),
379                count: 20,
380            },
381        ];
382
383        let renderer = OutputRenderer::new(OutputFormat::Table);
384        let result = renderer.render(&test_data);
385        assert!(result.is_ok());
386    }
387
388    #[test]
389    fn test_render_csv() {
390        let test_data = vec![
391            TestStruct {
392                id: "1".to_string(),
393                name: "Alice".to_string(),
394                count: 10,
395            },
396            TestStruct {
397                id: "2".to_string(),
398                name: "Bob".to_string(),
399                count: 20,
400            },
401        ];
402
403        let renderer = OutputRenderer::new(OutputFormat::Csv);
404        let result = renderer.render(&test_data);
405        assert!(result.is_ok());
406    }
407}