1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use anyhow::Result;
use clap::ValueEnum;
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Json,
Jq,
Jsonl,
Csv,
Table,
}
pub struct OutputFormatter;
impl OutputFormatter {
pub fn format(format: OutputFormat, data: &Value) -> Result<()> {
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(data)?);
}
OutputFormat::Jq => {
println!("{}", serde_json::to_string(data)?);
}
OutputFormat::Jsonl => {
if let Some(arr) = data.as_array() {
for item in arr {
println!("{}", serde_json::to_string(item)?);
}
} else {
println!("{}", serde_json::to_string(data)?);
}
}
OutputFormat::Csv => {
let mut wtr = csv::Writer::from_writer(std::io::stdout());
if let Some(arr) = data.as_array() {
let header_index =
arr.iter()
.position(|item| item.is_object())
.ok_or_else(|| {
anyhow::anyhow!(
"Cannot format array without object elements as CSV"
)
})?;
let header_obj = arr[header_index]
.as_object()
.expect("header_index points to an object element");
let headers: Vec<&str> = header_obj.keys().map(|s| s.as_str()).collect();
wtr.write_record(&headers)?;
for item in arr {
let obj = item.as_object().ok_or_else(|| {
anyhow::anyhow!(
"Cannot format array containing non-object elements as CSV"
)
})?;
let row: Vec<String> = headers
.iter()
.map(|key| match obj.get(*key) {
Some(Value::String(s)) => s.clone(),
Some(Value::Null) | None => String::new(),
Some(v) => v.to_string(),
})
.collect();
wtr.write_record(&row)?;
}
} else if let Some(obj) = data.as_object() {
let headers: Vec<&str> = obj.keys().map(|s| s.as_str()).collect();
wtr.write_record(&headers)?;
let row: Vec<String> = obj
.values()
.map(|v| match v {
Value::String(s) => s.clone(),
_ => v.to_string(),
})
.collect();
wtr.write_record(&row)?;
} else {
anyhow::bail!("Cannot format non-object/array as CSV");
}
wtr.flush()?;
}
OutputFormat::Table => {
let items = match data {
Value::Array(arr) => arr.clone(),
Value::Object(_) => vec![data.clone()],
_ => anyhow::bail!("Cannot format scalar value as Table"),
};
if items.is_empty() {
return Ok(());
}
if let Some(first_obj) = items[0].as_object() {
let keys: Vec<&str> = first_obj.keys().map(|s| s.as_str()).collect();
let mut widths = vec![0; keys.len()];
for (i, key) in keys.iter().enumerate() {
widths[i] = key.len();
}
let mut rows = vec![];
for item in &items {
if let Some(obj) = item.as_object() {
let mut row = vec![];
for (i, key) in keys.iter().enumerate() {
let val = obj
.get(*key)
.map(|v| match v {
Value::String(s) => s.clone(),
Value::Null => String::new(),
_ => v.to_string(),
})
.unwrap_or_default();
let val_display = if val.chars().count() > 60 {
let truncated: String = val.chars().take(57).collect();
format!("{truncated}...")
} else {
val
};
widths[i] = widths[i].max(val_display.len());
row.push(val_display);
}
rows.push(row);
}
}
// Print Headers
let header_row = keys
.iter()
.enumerate()
.map(|(i, k)| format!("{k:<w$}", k = k.to_uppercase(), w = widths[i]))
.collect::<Vec<_>>()
.join(" ");
println!("{header_row}");
println!("{}", "-".repeat(header_row.len()));
// Print Rows
for row in rows {
let formatted_row = row
.iter()
.enumerate()
.map(|(i, col)| format!("{col:<w$}", col = col, w = widths[i]))
.collect::<Vec<_>>()
.join(" ");
println!("{formatted_row}");
}
} else {
anyhow::bail!("Array elements must be objects to render as a table");
}
}
}
Ok(())
}
}