1use crate::api::ApiResponse;
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum OutputFormat {
9 Json,
10 Jsonl,
11 Table,
12 Tsv,
13}
14
15impl OutputFormat {
16 pub fn parse(s: &str) -> Result<Self, String> {
17 match s {
18 "json" => Ok(OutputFormat::Json),
19 "jsonl" => Ok(OutputFormat::Jsonl),
20 "table" => Ok(OutputFormat::Table),
21 "tsv" => Ok(OutputFormat::Tsv),
22 _ => Err(format!(
23 "Invalid format '{}'. Valid values: json, jsonl, table, tsv",
24 s
25 )),
26 }
27 }
28}
29
30impl fmt::Display for OutputFormat {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 OutputFormat::Json => write!(f, "json"),
34 OutputFormat::Jsonl => write!(f, "jsonl"),
35 OutputFormat::Table => write!(f, "table"),
36 OutputFormat::Tsv => write!(f, "tsv"),
37 }
38 }
39}
40
41pub fn format_response(response: &ApiResponse, format: OutputFormat) -> Result<String, String> {
43 match format {
44 OutputFormat::Json => serde_json::to_string_pretty(&response)
45 .map_err(|e| format!("Failed to serialize JSON: {}", e)),
46 OutputFormat::Jsonl => {
47 if let Some(channels) = response.data.get("channels") {
48 if let Some(channels_array) = channels.as_array() {
49 let lines: Vec<String> = channels_array
50 .iter()
51 .filter_map(|conv| serde_json::to_string(conv).ok())
52 .collect();
53 Ok(lines.join("\n"))
54 } else {
55 Ok(String::new())
56 }
57 } else {
58 Ok(String::new())
59 }
60 }
61 OutputFormat::Table => format_as_table(response),
62 OutputFormat::Tsv => format_as_tsv(response),
63 }
64}
65
66fn format_as_table(response: &ApiResponse) -> Result<String, String> {
68 let channels = match response.data.get("channels").and_then(|v| v.as_array()) {
69 Some(ch) => ch,
70 None => return Ok(String::new()),
71 };
72
73 if channels.is_empty() {
74 return Ok(String::new());
75 }
76
77 let mut max_id = "ID".len();
79 let mut max_name = "NAME".len();
80 let max_private = "PRIVATE".len();
81 let max_member = "MEMBER".len();
82 let mut max_num_members = "NUM_MEMBERS".len();
83
84 for conv in channels {
85 if let Some(id) = conv.get("id").and_then(|v| v.as_str()) {
86 max_id = max_id.max(id.len());
87 }
88 if let Some(name) = conv.get("name").and_then(|v| v.as_str()) {
89 max_name = max_name.max(name.len());
90 }
91 if let Some(num) = conv.get("num_members").and_then(|v| v.as_i64()) {
92 max_num_members = max_num_members.max(num.to_string().len());
93 }
94 }
95
96 let mut output = String::new();
98 output.push_str(&format!(
99 "{:width_id$} {:width_name$} {:width_private$} {:width_member$} {:width_num$}\n",
100 "ID",
101 "NAME",
102 "PRIVATE",
103 "MEMBER",
104 "NUM_MEMBERS",
105 width_id = max_id,
106 width_name = max_name,
107 width_private = max_private,
108 width_member = max_member,
109 width_num = max_num_members,
110 ));
111
112 output.push_str(&format!(
114 "{} {} {} {} {}\n",
115 "-".repeat(max_id),
116 "-".repeat(max_name),
117 "-".repeat(max_private),
118 "-".repeat(max_member),
119 "-".repeat(max_num_members),
120 ));
121
122 for conv in channels {
124 let id = conv.get("id").and_then(|v| v.as_str()).unwrap_or("");
125 let name = conv.get("name").and_then(|v| v.as_str()).unwrap_or("");
126 let is_private = conv
127 .get("is_private")
128 .and_then(|v| v.as_bool())
129 .unwrap_or(false);
130 let is_member = conv
131 .get("is_member")
132 .and_then(|v| v.as_bool())
133 .unwrap_or(false);
134 let num_members = conv.get("num_members").and_then(|v| v.as_i64());
135
136 let num_members_str = num_members.map(|n| n.to_string()).unwrap_or_default();
137
138 output.push_str(&format!(
139 "{:width_id$} {:width_name$} {:width_private$} {:width_member$} {:width_num$}\n",
140 id,
141 name,
142 is_private,
143 is_member,
144 num_members_str,
145 width_id = max_id,
146 width_name = max_name,
147 width_private = max_private,
148 width_member = max_member,
149 width_num = max_num_members,
150 ));
151 }
152
153 Ok(output)
154}
155
156fn format_as_tsv(response: &ApiResponse) -> Result<String, String> {
158 let channels = match response.data.get("channels").and_then(|v| v.as_array()) {
159 Some(ch) => ch,
160 None => return Ok(String::new()),
161 };
162
163 if channels.is_empty() {
164 return Ok(String::new());
165 }
166
167 let mut output = String::new();
168
169 output.push_str("id\tname\tis_private\tis_member\tnum_members\n");
171
172 for conv in channels {
174 let id = conv.get("id").and_then(|v| v.as_str()).unwrap_or("");
175 let name = conv.get("name").and_then(|v| v.as_str()).unwrap_or("");
176 let is_private = conv
177 .get("is_private")
178 .and_then(|v| v.as_bool())
179 .unwrap_or(false);
180 let is_member = conv
181 .get("is_member")
182 .and_then(|v| v.as_bool())
183 .unwrap_or(false);
184 let num_members = conv.get("num_members").and_then(|v| v.as_i64());
185
186 let num_members_str = num_members.map(|n| n.to_string()).unwrap_or_default();
187
188 output.push_str(&format!(
189 "{}\t{}\t{}\t{}\t{}\n",
190 id, name, is_private, is_member, num_members_str
191 ));
192 }
193
194 Ok(output)
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use serde_json::json;
201 use std::collections::HashMap;
202
203 #[test]
204 fn test_output_format_parse() {
205 assert_eq!(OutputFormat::parse("json").unwrap(), OutputFormat::Json);
206 assert_eq!(OutputFormat::parse("jsonl").unwrap(), OutputFormat::Jsonl);
207 assert_eq!(OutputFormat::parse("table").unwrap(), OutputFormat::Table);
208 assert_eq!(OutputFormat::parse("tsv").unwrap(), OutputFormat::Tsv);
209 assert!(OutputFormat::parse("invalid").is_err());
210 }
211
212 #[test]
213 fn test_format_response_jsonl() {
214 let response = ApiResponse {
215 ok: true,
216 data: HashMap::from([(
217 "channels".to_string(),
218 json!([
219 {"id": "C1", "name": "general"},
220 {"id": "C2", "name": "random"},
221 ]),
222 )]),
223 error: None,
224 };
225
226 let output = format_response(&response, OutputFormat::Jsonl).unwrap();
227 let lines: Vec<&str> = output.lines().collect();
228 assert_eq!(lines.len(), 2);
229 assert!(lines[0].contains("\"id\":\"C1\""));
230 assert!(lines[1].contains("\"id\":\"C2\""));
231 }
232
233 #[test]
234 fn test_format_response_tsv() {
235 let response = ApiResponse {
236 ok: true,
237 data: HashMap::from([(
238 "channels".to_string(),
239 json!([
240 {"id": "C1", "name": "general", "is_private": false, "is_member": true, "num_members": 42},
241 {"id": "C2", "name": "private", "is_private": true, "is_member": false},
242 ]),
243 )]),
244 error: None,
245 };
246
247 let output = format_response(&response, OutputFormat::Tsv).unwrap();
248 let lines: Vec<&str> = output.lines().collect();
249 assert_eq!(lines.len(), 3); assert_eq!(lines[0], "id\tname\tis_private\tis_member\tnum_members");
251 assert_eq!(lines[1], "C1\tgeneral\tfalse\ttrue\t42");
252 assert_eq!(lines[2], "C2\tprivate\ttrue\tfalse\t"); }
254
255 #[test]
256 fn test_format_response_table() {
257 let response = ApiResponse {
258 ok: true,
259 data: HashMap::from([(
260 "channels".to_string(),
261 json!([
262 {"id": "C1", "name": "general", "is_private": false, "is_member": true, "num_members": 42},
263 ]),
264 )]),
265 error: None,
266 };
267
268 let output = format_response(&response, OutputFormat::Table).unwrap();
269 assert!(output.contains("ID"));
270 assert!(output.contains("NAME"));
271 assert!(output.contains("PRIVATE"));
272 assert!(output.contains("MEMBER"));
273 assert!(output.contains("NUM_MEMBERS"));
274 assert!(output.contains("C1"));
275 assert!(output.contains("general"));
276 assert!(output.contains("42"));
277 }
278}