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}