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