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
56pub 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 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}