1use std::io::{self, Write};
5
6use taba_client::json::{self, Fields};
7use taba_client::Error as ApiError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Format {
11 Json,
12 Table,
13 Raw,
14}
15
16impl Format {
17 pub fn parse(name: &str) -> Result<Format, String> {
18 match name {
19 "json" => Ok(Format::Json),
20 "table" => Ok(Format::Table),
21 "raw" => Ok(Format::Raw),
22 other => Err(format!(
23 "unknown output format `{other}` (use json, table or raw)"
24 )),
25 }
26 }
27}
28
29pub fn render(text: &str, format: Format) -> String {
31 match format {
32 Format::Json => json::pretty(text).unwrap_or_else(|_| text.trim_end().to_owned()),
33 Format::Table => table(text).unwrap_or_else(|| render(text, Format::Json)),
34 Format::Raw => text.trim_end().to_owned(),
35 }
36}
37
38pub fn table(text: &str) -> Option<String> {
41 let text = text.trim();
42 let rows: Vec<&str>;
43 let mut single = false;
44 if text.starts_with('[') {
45 rows = json::array_items(text).ok()?;
46 } else if text.starts_with('{') {
47 let fields = Fields::parse(text).ok()?;
48 match fields.raw("items").or_else(|| fields.raw("messages")) {
49 Some(list) if list.starts_with('[') => rows = json::array_items(list).ok()?,
50 _ => {
51 rows = vec![text];
52 single = true;
53 }
54 }
55 } else {
56 return None;
57 }
58 if rows.is_empty() {
59 return Some("(empty)".to_owned());
60 }
61 if single {
62 let fields = json::object_fields(rows[0]).ok()?;
63 let table: Vec<Vec<String>> = fields
64 .iter()
65 .map(|(k, v)| vec![k.clone(), cell(v)])
66 .collect();
67 return Some(layout(&["field", "value"], &table));
68 }
69 let mut columns: Vec<String> = Vec::new();
70 let mut parsed = Vec::new();
71 for row in &rows {
72 let fields = json::object_fields(row).ok()?;
73 for (k, _) in &fields {
74 if !columns.contains(k) {
75 columns.push(k.clone());
76 }
77 }
78 parsed.push(fields);
79 }
80 let table: Vec<Vec<String>> = parsed
81 .iter()
82 .map(|fields| {
83 columns
84 .iter()
85 .map(|c| {
86 fields
87 .iter()
88 .find(|(k, _)| k == c)
89 .map(|(_, v)| cell(v))
90 .unwrap_or_else(|| "-".to_owned())
91 })
92 .collect()
93 })
94 .collect();
95 let header: Vec<&str> = columns.iter().map(String::as_str).collect();
96 Some(layout(&header, &table))
97}
98
99pub fn cluster_table(text: &str) -> Option<String> {
101 let fields = Fields::parse(text.trim()).ok()?;
102 let simple: Vec<Vec<String>> = [
103 "scope",
104 "partial",
105 "worstCondition",
106 "collectedAt",
107 "summary",
108 ]
109 .iter()
110 .filter_map(|name| fields.raw(name).map(|v| vec![(*name).to_owned(), cell(v)]))
111 .collect();
112 let mut out = layout(&["field", "value"], &simple);
113 for (title, name) in [("nodes", "nodes"), ("conditions", "conditions")] {
114 let list = fields.raw(name).filter(|l| l.starts_with('['))?;
115 out.push_str(&format!("\n\n{title}\n"));
116 out.push_str(&table(list)?);
117 }
118 Some(out)
119}
120
121fn cell(raw: &str) -> String {
123 let raw = raw.trim();
124 let text = if raw.starts_with('"') {
125 json::decode_string(raw).unwrap_or_else(|_| raw.to_owned())
126 } else if raw == "null" {
127 "-".to_owned()
128 } else if raw.starts_with('{') || raw.starts_with('[') {
129 compact(raw)
130 } else {
131 raw.to_owned()
132 };
133 let text = text.replace(['\n', '\r', '\t'], " ");
134 if text.chars().count() > 60 {
135 format!("{}…", text.chars().take(59).collect::<String>())
136 } else {
137 text
138 }
139}
140
141fn compact(raw: &str) -> String {
143 let mut out = String::with_capacity(raw.len());
144 let mut in_string = false;
145 let mut escaped = false;
146 for c in raw.chars() {
147 if in_string {
148 out.push(c);
149 if escaped {
150 escaped = false;
151 } else if c == '\\' {
152 escaped = true;
153 } else if c == '"' {
154 in_string = false;
155 }
156 } else if c == '"' {
157 in_string = true;
158 out.push(c);
159 } else if !c.is_whitespace() {
160 out.push(c);
161 }
162 }
163 out
164}
165
166fn layout(header: &[&str], rows: &[Vec<String>]) -> String {
167 let widths: Vec<usize> = (0..header.len())
168 .map(|i| {
169 rows.iter()
170 .map(|r| r[i].chars().count())
171 .chain(std::iter::once(header[i].chars().count()))
172 .max()
173 .unwrap_or(0)
174 })
175 .collect();
176 let line = |cells: Vec<&str>| {
177 let mut out = String::new();
178 for (i, c) in cells.iter().enumerate() {
179 if i + 1 == cells.len() {
180 out.push_str(c);
181 } else {
182 out.push_str(&format!("{c:<width$} ", width = widths[i]));
183 }
184 }
185 out.trim_end().to_owned()
186 };
187 let mut lines = vec![line(header.to_vec())];
188 for row in rows {
189 lines.push(line(row.iter().map(String::as_str).collect()));
190 }
191 lines.join("\n")
192}
193
194pub fn write_api_error(w: &mut dyn Write, e: &ApiError, json: bool) -> io::Result<()> {
197 if json && e.status > 0 {
198 let body = String::from_utf8_lossy(&e.body);
199 if Fields::parse(&body).is_ok_and(|f| f.string("code").is_some()) {
200 writeln!(w, "{}", render(&body, Format::Json))?;
201 if let Some(n) = e.retry_after {
202 writeln!(w, "Retry-After: {n}")?;
203 }
204 return Ok(());
205 }
206 }
207 writeln!(w, "{e}")?;
208 if let Some(details) = &e.details {
209 for item in json::array_items(details).unwrap_or_default() {
210 if let Ok(f) = Fields::parse(item) {
211 let field = f.string("field").unwrap_or_default();
212 let message = f.string("message").unwrap_or_default();
213 let code = f.string("code").unwrap_or_default();
214 writeln!(w, " - {field}: {message} ({code})")?;
215 }
216 }
217 }
218 if let Some(n) = e.retry_after {
219 writeln!(w, "Retry-After: {n} s")?;
220 }
221 Ok(())
222}
223
224#[cfg(test)]
225mod tests;