gr/
display.rs

1use crate::remote::GetRemoteCliArgs;
2use crate::Result;
3use std::{collections::HashMap, io::Write};
4
5#[derive(Clone, Debug, Default)]
6pub enum Format {
7    CSV,
8    JSON,
9    #[default]
10    PIPE,
11    TOML,
12}
13
14impl From<Format> for u8 {
15    fn from(f: Format) -> Self {
16        match f {
17            Format::CSV => b',',
18            Format::PIPE => b'|',
19            Format::JSON => 0,
20            Format::TOML => 0,
21        }
22    }
23}
24
25pub struct DisplayBody {
26    pub columns: Vec<Column>,
27}
28
29impl DisplayBody {
30    pub fn new(columns: Vec<Column>) -> Self {
31        Self { columns }
32    }
33}
34
35#[derive(Builder)]
36pub struct Column {
37    pub name: String,
38    pub value: String,
39    #[builder(default)]
40    pub optional: bool,
41}
42
43impl Column {
44    pub fn builder() -> ColumnBuilder {
45        ColumnBuilder::default()
46    }
47    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
48        Self {
49            name: name.into(),
50            value: value.into(),
51            optional: false,
52        }
53    }
54}
55
56// TODO: Change args to borrow GetRemoteCliArgs
57pub fn print<W: Write, D: Into<DisplayBody> + Clone>(
58    w: &mut W,
59    data: Vec<D>,
60    args: GetRemoteCliArgs,
61) -> Result<()> {
62    if data.is_empty() {
63        return Ok(());
64    }
65    match args.format {
66        Format::JSON => {
67            for d in data {
68                let d = d.into();
69                let kvs: HashMap<String, String> = d
70                    .columns
71                    .into_iter()
72                    .filter(|c| !c.optional || args.display_optional)
73                    .map(|item| (item.name.to_lowercase(), item.value))
74                    .collect();
75                writeln!(w, "{}", serde_json::to_string(&kvs)?)?;
76            }
77        }
78        Format::TOML => {
79            writeln!(w, "[")?;
80            let data_len = data.len();
81            for (index, d) in data.into_iter().enumerate() {
82                let d = d.into();
83                write!(w, "    {{")?;
84                let mut first = true;
85                for column in d.columns {
86                    if !column.optional || args.display_optional {
87                        if !first {
88                            write!(w, ",")?;
89                        }
90                        write!(w, " {} = {:?}", column.name.to_lowercase(), column.value)?;
91                        first = false;
92                    }
93                }
94                write!(w, " }}")?;
95                if index < data_len - 1 {
96                    writeln!(w, ",")?;
97                } else {
98                    writeln!(w)?;
99                }
100            }
101            writeln!(w, "]")?;
102        }
103        _ => {
104            let mut wtr = csv::WriterBuilder::new()
105                .delimiter(args.format.into())
106                .from_writer(w);
107            if !args.no_headers {
108                // Get the headers from the first row of columns
109                let headers = data[0]
110                    .clone()
111                    .into()
112                    .columns
113                    .iter()
114                    .filter(|c| !c.optional || args.display_optional)
115                    .map(|c| c.name.clone())
116                    .collect::<Vec<_>>();
117                wtr.write_record(&headers)?;
118            }
119            for d in data {
120                let d = d.into();
121                let row = d
122                    .columns
123                    .into_iter()
124                    .filter(|c| !c.optional || args.display_optional)
125                    .map(|c| c.value)
126                    .collect::<Vec<_>>();
127                wtr.write_record(&row)?;
128            }
129            wtr.flush()?;
130        }
131    }
132    Ok(())
133}
134
135#[cfg(test)]
136mod test {
137    use super::*;
138
139    #[derive(Clone)]
140    struct Book {
141        pub title: String,
142        pub author: String,
143    }
144
145    impl Book {
146        pub fn new(title: impl Into<String>, author: impl Into<String>) -> Self {
147            Self {
148                title: title.into(),
149                author: author.into(),
150            }
151        }
152    }
153
154    impl From<Book> for DisplayBody {
155        fn from(b: Book) -> Self {
156            DisplayBody::new(vec![
157                Column::new("title", b.title),
158                Column::new("author", b.author),
159            ])
160        }
161    }
162
163    #[test]
164    fn test_json() {
165        let mut w = Vec::new();
166        let books = vec![
167            Book::new("The Catcher in the Rye", "J.D. Salinger"),
168            Book::new("The Adventures of Huckleberry Finn", "Mark Twain"),
169        ];
170        let args = GetRemoteCliArgs::builder()
171            .no_headers(true)
172            .format(Format::JSON)
173            .build()
174            .unwrap();
175        print(&mut w, books, args).unwrap();
176        let s = String::from_utf8(w).unwrap();
177        assert_eq!(2, s.lines().count());
178        for line in s.lines() {
179            let v: serde_json::Value = serde_json::from_str(line).unwrap();
180            assert!(v.is_object());
181            let obj = v.as_object().unwrap();
182            assert_eq!(obj.len(), 2);
183            assert!(obj.contains_key("title"));
184            assert!(obj.contains_key("author"));
185        }
186    }
187
188    #[test]
189    fn test_csv_multiple_commas_one_field() {
190        let mut w = Vec::new();
191        let books = vec![
192            Book::new("Faust, Part One", "Goethe"),
193            Book::new("The Adventures of Huckleberry Finn", "Mark Twain"),
194        ];
195        let args = GetRemoteCliArgs::builder()
196            .no_headers(true)
197            .format(Format::CSV)
198            .build()
199            .unwrap();
200        print(&mut w, books, args).unwrap();
201        let mut reader = csv::ReaderBuilder::new()
202            .has_headers(false)
203            .from_reader(w.as_slice());
204        assert_eq!(
205            "Faust, Part One",
206            &reader.records().next().unwrap().unwrap()[0]
207        );
208    }
209
210    #[derive(Clone)]
211    struct BookOptionalColumns {
212        pub title: String,
213        pub author: String,
214        pub isbn: String,
215    }
216
217    impl BookOptionalColumns {
218        pub fn new(
219            title: impl Into<String>,
220            author: impl Into<String>,
221            isbn: impl Into<String>,
222        ) -> Self {
223            Self {
224                title: title.into(),
225                author: author.into(),
226                isbn: isbn.into(),
227            }
228        }
229    }
230
231    impl From<BookOptionalColumns> for DisplayBody {
232        fn from(b: BookOptionalColumns) -> Self {
233            DisplayBody::new(vec![
234                Column::new("title", b.title),
235                Column::new("author", b.author),
236                Column::builder()
237                    .name("isbn".to_string())
238                    .value(b.isbn)
239                    .optional(true)
240                    .build()
241                    .unwrap(),
242            ])
243        }
244    }
245
246    #[test]
247    fn test_csv_optional_columns() {
248        let mut w = Vec::new();
249        let books = vec![
250            BookOptionalColumns::new("The Catcher in the Rye", "J.D. Salinger", "0316769487"),
251            BookOptionalColumns::new(
252                "The Adventures of Huckleberry Finn",
253                "Mark Twain",
254                "9780199536559",
255            ),
256        ];
257        let args = GetRemoteCliArgs::builder()
258            .format(Format::CSV)
259            .build()
260            .unwrap();
261        print(&mut w, books, args).unwrap();
262        assert_eq!(
263            "title,author\nThe Catcher in the Rye,J.D. Salinger\nThe Adventures of Huckleberry Finn,Mark Twain\n",
264            String::from_utf8(w).unwrap()
265        );
266    }
267
268    #[test]
269    fn test_csv_display_optional_columns_on_args() {
270        let mut w = Vec::new();
271        let books = vec![
272            BookOptionalColumns::new("The Catcher in the Rye", "J.D. Salinger", "0316769487"),
273            BookOptionalColumns::new(
274                "The Adventures of Huckleberry Finn",
275                "Mark Twain",
276                "9780199536559",
277            ),
278        ];
279        let args = GetRemoteCliArgs::builder()
280            .format(Format::CSV)
281            .display_optional(true)
282            .build()
283            .unwrap();
284        print(&mut w, books, args).unwrap();
285        assert_eq!(
286            "title,author,isbn\nThe Catcher in the Rye,J.D. Salinger,0316769487\nThe Adventures of Huckleberry Finn,Mark Twain,9780199536559\n",
287            String::from_utf8(w).unwrap()
288        );
289    }
290
291    #[test]
292    fn test_toml_single_row() {
293        let mut w = Vec::new();
294        let books = vec![Book::new("The Catcher in the Rye", "J.D. Salinger")];
295        let args = GetRemoteCliArgs::builder()
296            .format(Format::TOML)
297            .build()
298            .unwrap();
299        print(&mut w, books, args).unwrap();
300        let s = String::from_utf8(w).unwrap();
301        assert_eq!(
302            s,
303            "[\n    { title = \"The Catcher in the Rye\", author = \"J.D. Salinger\" }\n]\n"
304        );
305    }
306
307    #[test]
308    fn test_toml_multiple_rows() {
309        let mut w = Vec::new();
310        let books = vec![
311            Book::new("The Catcher in the Rye", "J.D. Salinger"),
312            Book::new("The Adventures of Huckleberry Finn", "Mark Twain"),
313        ];
314        let args = GetRemoteCliArgs::builder()
315            .format(Format::TOML)
316            .build()
317            .unwrap();
318        print(&mut w, books, args).unwrap();
319        let s = String::from_utf8(w).unwrap();
320        assert_eq!(s, "[\n    { title = \"The Catcher in the Rye\", author = \"J.D. Salinger\" },\n    { title = \"The Adventures of Huckleberry Finn\", author = \"Mark Twain\" }\n]\n");
321    }
322
323    #[test]
324    fn test_toml_optional_columns() {
325        let mut w = Vec::new();
326        let books = vec![
327            BookOptionalColumns::new("The Catcher in the Rye", "J.D. Salinger", "0316769487"),
328            BookOptionalColumns::new(
329                "The Adventures of Huckleberry Finn",
330                "Mark Twain",
331                "9780199536559",
332            ),
333        ];
334        let args = GetRemoteCliArgs::builder()
335            .format(Format::TOML)
336            .build()
337            .unwrap();
338        print(&mut w, books, args).unwrap();
339        let s = String::from_utf8(w).unwrap();
340        assert_eq!(s, "[\n    { title = \"The Catcher in the Rye\", author = \"J.D. Salinger\" },\n    { title = \"The Adventures of Huckleberry Finn\", author = \"Mark Twain\" }\n]\n");
341    }
342}