1use anyhow::Result;
10use console::Term;
11use serde::Serialize;
12use std::str::FromStr;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum OutputFormat {
17 Table,
19 Json,
21 Yaml,
23 Csv,
25 Plain,
27}
28
29impl FromStr for OutputFormat {
30 type Err = anyhow::Error;
31
32 fn from_str(s: &str) -> Result<Self, Self::Err> {
33 match s.to_lowercase().as_str() {
34 "table" => Ok(OutputFormat::Table),
35 "json" => Ok(OutputFormat::Json),
36 "yaml" | "yml" => Ok(OutputFormat::Yaml),
37 "csv" => Ok(OutputFormat::Csv),
38 "plain" => Ok(OutputFormat::Plain),
39 _ => anyhow::bail!(
40 "Invalid output format: {}. Use: table, json, yaml, csv, plain",
41 s
42 ),
43 }
44 }
45}
46
47impl OutputFormat {
48 pub fn determine(cli_format: Option<OutputFormat>) -> OutputFormat {
50 if let Some(format) = cli_format {
52 return format;
53 }
54
55 if let Ok(env_format) = std::env::var("RAPS_OUTPUT_FORMAT")
57 && let Ok(format) = OutputFormat::from_str(&env_format)
58 {
59 return format;
60 }
61
62 if !Term::stdout().is_term() {
64 return OutputFormat::Json;
65 }
66
67 OutputFormat::Table
69 }
70
71 pub fn write<T: Serialize>(&self, data: &T) -> Result<()> {
73 match self {
74 OutputFormat::Table => write_table(data),
75 OutputFormat::Json => write_json(data),
76 OutputFormat::Yaml => write_yaml(data),
77 OutputFormat::Csv => write_csv(data),
78 OutputFormat::Plain => write_plain(data),
79 }
80 }
81
82 pub fn write_message(&self, message: &str) -> Result<()> {
84 match self {
85 OutputFormat::Table | OutputFormat::Plain => {
86 println!("{}", message);
87 Ok(())
88 }
89 OutputFormat::Json => {
90 #[derive(Serialize)]
91 struct Message {
92 message: String,
93 }
94 write_json(&Message {
95 message: message.to_string(),
96 })
97 }
98 OutputFormat::Yaml => {
99 #[derive(Serialize)]
100 struct Message {
101 message: String,
102 }
103 write_yaml(&Message {
104 message: message.to_string(),
105 })
106 }
107 OutputFormat::Csv => {
108 println!("{}", message);
110 Ok(())
111 }
112 }
113 }
114
115 pub fn supports_colors(&self) -> bool {
117 matches!(self, OutputFormat::Table)
118 }
119}
120
121fn write_json<T: Serialize>(data: &T) -> Result<()> {
123 let json = serde_json::to_string_pretty(data)?;
124 println!("{}", json);
125 Ok(())
126}
127
128fn write_yaml<T: Serialize>(data: &T) -> Result<()> {
130 let yaml = serde_yaml::to_string(data)?;
131 print!("{}", yaml);
132 Ok(())
133}
134
135fn write_csv<T: Serialize>(data: &T) -> Result<()> {
137 let json_value = serde_json::to_value(data)?;
139
140 match json_value {
141 serde_json::Value::Array(items) if !items.is_empty() => {
142 if let Some(serde_json::Value::Object(map)) = items.first() {
144 let mut wtr = csv::Writer::from_writer(std::io::stdout());
145
146 let headers: Vec<String> = map.keys().cloned().collect();
148 wtr.write_record(&headers)?;
149
150 for item in items {
152 if let serde_json::Value::Object(map) = item {
153 let mut row = Vec::new();
154 for header in &headers {
155 let value = map.get(header).unwrap_or(&serde_json::Value::Null);
156 row.push(format_value_for_csv(value));
157 }
158 wtr.write_record(&row)?;
159 }
160 }
161 wtr.flush()?;
162 return Ok(());
163 }
164 }
165 _ => {
166 return write_json(data);
168 }
169 }
170
171 write_json(data)
173}
174
175fn format_value_for_csv(value: &serde_json::Value) -> String {
177 match value {
178 serde_json::Value::Null => String::new(),
179 serde_json::Value::Bool(b) => b.to_string(),
180 serde_json::Value::Number(n) => n.to_string(),
181 serde_json::Value::String(s) => s.clone(),
182 serde_json::Value::Array(arr) => {
183 arr.iter()
185 .map(format_value_for_csv)
186 .collect::<Vec<_>>()
187 .join("; ")
188 }
189 serde_json::Value::Object(obj) => {
190 serde_json::to_string(obj).unwrap_or_default()
192 }
193 }
194}
195
196fn write_plain<T: Serialize>(data: &T) -> Result<()> {
198 let json = serde_json::to_string_pretty(data)?;
201 println!("{}", json);
202 Ok(())
203}
204
205fn write_table<T: Serialize>(data: &T) -> Result<()> {
207 write_json(data)
211}
212
213#[allow(dead_code)] pub trait TableFormat {
216 fn write_table(&self) -> Result<()>;
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[derive(Serialize)]
225 #[allow(dead_code)]
226 struct TestData {
227 id: String,
228 name: String,
229 count: u32,
230 }
231
232 #[derive(Serialize)]
233 #[allow(dead_code)]
234 struct NestedData {
235 id: String,
236 items: Vec<String>,
237 }
238
239 #[test]
240 fn test_output_format_from_str_table() {
241 let format = OutputFormat::from_str("table").unwrap();
242 assert_eq!(format, OutputFormat::Table);
243 }
244
245 #[test]
246 fn test_output_format_from_str_json() {
247 let format = OutputFormat::from_str("json").unwrap();
248 assert_eq!(format, OutputFormat::Json);
249 }
250
251 #[test]
252 fn test_output_format_from_str_yaml() {
253 let format = OutputFormat::from_str("yaml").unwrap();
254 assert_eq!(format, OutputFormat::Yaml);
255 }
256
257 #[test]
258 fn test_output_format_from_str_yml() {
259 let format = OutputFormat::from_str("yml").unwrap();
260 assert_eq!(format, OutputFormat::Yaml);
261 }
262
263 #[test]
264 fn test_output_format_from_str_csv() {
265 let format = OutputFormat::from_str("csv").unwrap();
266 assert_eq!(format, OutputFormat::Csv);
267 }
268
269 #[test]
270 fn test_output_format_from_str_plain() {
271 let format = OutputFormat::from_str("plain").unwrap();
272 assert_eq!(format, OutputFormat::Plain);
273 }
274
275 #[test]
276 fn test_output_format_from_str_case_insensitive() {
277 assert_eq!(OutputFormat::from_str("JSON").unwrap(), OutputFormat::Json);
278 assert_eq!(
279 OutputFormat::from_str("Table").unwrap(),
280 OutputFormat::Table
281 );
282 assert_eq!(OutputFormat::from_str("YAML").unwrap(), OutputFormat::Yaml);
283 }
284
285 #[test]
286 fn test_output_format_from_str_invalid() {
287 let result = OutputFormat::from_str("invalid");
288 assert!(result.is_err());
289 }
290
291 #[test]
292 fn test_supports_colors_table() {
293 assert!(OutputFormat::Table.supports_colors());
294 }
295
296 #[test]
297 fn test_supports_colors_json() {
298 assert!(!OutputFormat::Json.supports_colors());
299 }
300
301 #[test]
302 fn test_supports_colors_yaml() {
303 assert!(!OutputFormat::Yaml.supports_colors());
304 }
305
306 #[test]
307 fn test_supports_colors_csv() {
308 assert!(!OutputFormat::Csv.supports_colors());
309 }
310
311 #[test]
312 fn test_supports_colors_plain() {
313 assert!(!OutputFormat::Plain.supports_colors());
314 }
315
316 #[test]
317 fn test_determine_explicit_format() {
318 let format = OutputFormat::determine(Some(OutputFormat::Json));
319 assert_eq!(format, OutputFormat::Json);
320 }
321
322 #[test]
323 fn test_determine_env_format() {
324 unsafe {
326 std::env::set_var("RAPS_OUTPUT_FORMAT", "yaml");
327 }
328 let _format = OutputFormat::determine(None);
329 unsafe {
331 std::env::remove_var("RAPS_OUTPUT_FORMAT");
332 }
333 }
335
336 #[test]
337 fn test_format_value_for_csv_null() {
338 let value = serde_json::Value::Null;
339 assert_eq!(format_value_for_csv(&value), "");
340 }
341
342 #[test]
343 fn test_format_value_for_csv_bool() {
344 let value = serde_json::Value::Bool(true);
345 assert_eq!(format_value_for_csv(&value), "true");
346 }
347
348 #[test]
349 fn test_format_value_for_csv_number() {
350 let value = serde_json::json!(42);
351 assert_eq!(format_value_for_csv(&value), "42");
352 }
353
354 #[test]
355 fn test_format_value_for_csv_string() {
356 let value = serde_json::json!("hello");
357 assert_eq!(format_value_for_csv(&value), "hello");
358 }
359
360 #[test]
361 fn test_format_value_for_csv_array() {
362 let value = serde_json::json!(["a", "b", "c"]);
363 assert_eq!(format_value_for_csv(&value), "a; b; c");
364 }
365
366 #[test]
367 fn test_format_value_for_csv_object() {
368 let value = serde_json::json!({"key": "value"});
369 let result = format_value_for_csv(&value);
370 assert!(result.contains("key"));
371 assert!(result.contains("value"));
372 }
373}