Skip to main content

indodax_cli/output/
mod.rs

1use serde::Serialize;
2use std::fmt;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
5pub enum OutputFormat {
6    Table,
7    Json,
8}
9
10impl fmt::Display for OutputFormat {
11    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12        match self {
13            OutputFormat::Table => write!(f, "table"),
14            OutputFormat::Json => write!(f, "json"),
15        }
16    }
17}
18
19#[derive(Debug, Serialize)]
20pub struct CommandOutput {
21    pub data: serde_json::Value,
22    pub headers: Vec<String>,
23    pub rows: Vec<Vec<String>>,
24    #[serde(skip)]
25    pub format: OutputFormat,
26    #[serde(skip)]
27    pub addendum: Option<String>,
28    #[serde(skip_serializing_if = "Vec::is_empty")]
29    pub warnings: Vec<String>,
30    #[serde(skip)]
31    #[doc(hidden)]
32    pub suppress_final_output: bool,
33}
34
35impl CommandOutput {
36    pub fn new(
37        data: serde_json::Value,
38        headers: Vec<String>,
39        rows: Vec<Vec<String>>,
40    ) -> Self {
41        Self {
42            data,
43            headers,
44            rows,
45            format: OutputFormat::Table,
46            addendum: None,
47            warnings: vec![],
48            suppress_final_output: false,
49        }
50    }
51
52    pub fn json(data: serde_json::Value) -> Self {
53        Self {
54            data,
55            headers: vec![],
56            rows: vec![],
57            format: OutputFormat::Table,
58            addendum: None,
59            warnings: vec![],
60            suppress_final_output: false,
61        }
62    }
63
64    pub fn new_empty() -> Self {
65        Self {
66            data: serde_json::json!({}),
67            headers: vec![],
68            rows: vec![],
69            format: OutputFormat::Table,
70            addendum: None,
71            warnings: vec![],
72            suppress_final_output: false,
73        }
74    }
75
76    pub fn with_format(mut self, format: OutputFormat) -> Self {
77        self.format = format;
78        self
79    }
80
81    pub fn with_addendum(mut self, addendum: impl Into<String>) -> Self {
82        self.addendum = Some(addendum.into());
83        self
84    }
85
86    pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
87        self.warnings.push(warning.into());
88        self
89    }
90
91    pub fn add_warning(&mut self, warning: impl Into<String>) {
92        self.warnings.push(warning.into());
93    }
94
95    pub fn with_suppress_final_output(mut self, suppress: bool) -> Self {
96        self.suppress_final_output = suppress;
97        self
98    }
99
100    pub fn render(&self) -> String {
101        match self.format {
102            OutputFormat::Table => table::render(self),
103            OutputFormat::Json => json::render(self),
104        }
105    }
106}
107
108pub mod table;
109pub mod json;
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use serde_json::json;
115
116    #[test]
117    fn test_output_format_display_table() {
118        assert_eq!(format!("{}", OutputFormat::Table), "table");
119    }
120
121    #[test]
122    fn test_output_format_display_json() {
123        assert_eq!(format!("{}", OutputFormat::Json), "json");
124    }
125
126    #[test]
127    fn test_output_format_from_clap() {
128        // Test that OutputFormat works with clap ValueEnum
129        let formats = [OutputFormat::Table, OutputFormat::Json];
130        assert_eq!(formats.len(), 2);
131    }
132
133    #[test]
134    fn test_command_output_new() {
135        let data = json!({"key": "value"});
136        let headers = vec!["H1".into(), "H2".into()];
137        let rows = vec![vec!["a".into(), "b".into()]];
138        let output = CommandOutput::new(data.clone(), headers.clone(), rows.clone());
139        
140        assert_eq!(output.data, data);
141        assert_eq!(output.headers, headers);
142        assert_eq!(output.rows, rows);
143        assert_eq!(output.format, OutputFormat::Table);
144        assert!(output.addendum.is_none());
145    }
146
147    #[test]
148    fn test_command_output_json() {
149        let data = json!({"test": 123});
150        let output = CommandOutput::json(data.clone());
151        
152        assert_eq!(output.data, data);
153        assert!(output.headers.is_empty());
154        assert!(output.rows.is_empty());
155        assert_eq!(output.format, OutputFormat::Table);
156    }
157
158    #[test]
159    fn test_command_output_new_empty() {
160        let output = CommandOutput::new_empty();
161        
162        assert_eq!(output.data, json!({}));
163        assert!(output.headers.is_empty());
164        assert!(output.rows.is_empty());
165    }
166
167    #[test]
168    fn test_command_output_with_format() {
169        let output = CommandOutput::json(json!({"test": true}))
170            .with_format(OutputFormat::Json);
171        
172        assert_eq!(output.format, OutputFormat::Json);
173    }
174
175    #[test]
176    fn test_command_output_with_addendum() {
177        let output = CommandOutput::new_empty()
178            .with_addendum("test addendum");
179        
180        assert_eq!(output.addendum, Some("test addendum".into()));
181    }
182
183    #[test]
184    fn test_command_output_render_table() {
185        let output = CommandOutput {
186            data: json!({"key": "value"}),
187            headers: vec!["Key".into(), "Value".into()],
188            rows: vec![vec!["key".into(), "value".into()]],
189            format: OutputFormat::Table,
190            addendum: None,
191            warnings: vec![],
192            suppress_final_output: false,
193        };
194        
195        let rendered = output.render();
196        assert!(!rendered.is_empty());
197    }
198
199    #[test]
200    fn test_command_output_render_json() {
201        let data = json!({"hello": "world"});
202        let output = CommandOutput::json(data.clone()).with_format(OutputFormat::Json);
203        
204        let rendered = output.render();
205        let parsed: serde_json::Value = serde_json::from_str(&rendered).unwrap();
206        assert_eq!(parsed["data"], data);
207    }
208
209    #[test]
210    fn test_command_output_render_json_empty() {
211        let output = CommandOutput::new_empty().with_format(OutputFormat::Json);
212        let rendered = output.render();
213        let parsed: serde_json::Value = serde_json::from_str(&rendered).unwrap();
214        assert_eq!(parsed["data"], json!({}));
215    }
216
217    #[test]
218    fn test_command_output_render_table_with_addendum() {
219        let output = CommandOutput {
220            data: json!({}),
221            headers: vec![],
222            rows: vec![],
223            format: OutputFormat::Table,
224            addendum: Some("Extra info".into()),
225            warnings: vec![],
226            suppress_final_output: false,
227        };
228        
229        let rendered = output.render();
230        assert!(rendered.contains("Extra info"));
231    }
232
233    #[test]
234    fn test_command_output_serialization() {
235        let output = CommandOutput {
236            data: json!({"test": true}),
237            headers: vec!["H".into()],
238            rows: vec![vec!["v".into()]],
239            format: OutputFormat::Json,
240            addendum: Some("info".into()),
241            warnings: vec![],
242            suppress_final_output: false,
243        };
244        
245        let serialized = serde_json::to_string(&output).unwrap();
246        let deserialized: serde_json::Value = serde_json::from_str(&serialized).unwrap();
247        
248        assert_eq!(deserialized["data"]["test"], true);
249        assert_eq!(deserialized["headers"][0], "H");
250        assert_eq!(deserialized["rows"][0][0], "v");
251        // format and addendum should be skipped (marked with #[serde(skip)])
252    }
253
254    #[test]
255    fn test_command_output_render_empty_table_with_data() {
256        let output = CommandOutput {
257            data: json!({"key": "value"}),
258            headers: vec![],
259            rows: vec![],
260            format: OutputFormat::Table,
261            addendum: None,
262            warnings: vec![],
263            suppress_final_output: false,
264        };
265        
266        let rendered = output.render();
267        // Should fall back to rendering data as JSON
268        assert!(rendered.contains("key") || rendered.contains("value") || rendered.is_empty());
269    }
270}