Skip to main content

mkt_core/output/
mod.rs

1//! Output formatting for CLI results.
2//!
3//! Supports table (via `comfy-table`), JSON, and CSV output formats.
4//! All formatters operate on types implementing [`Formattable`].
5
6mod csv_fmt;
7mod json;
8mod table;
9
10use serde::Serialize;
11
12use crate::error::Result;
13
14/// Output format selection.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
16#[serde(rename_all = "lowercase")]
17pub enum OutputFormat {
18    /// Tabular output for terminal display.
19    #[default]
20    Table,
21    /// JSON output for machine consumption.
22    Json,
23    /// CSV output for spreadsheets.
24    Csv,
25}
26
27impl std::fmt::Display for OutputFormat {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            Self::Table => write!(f, "table"),
31            Self::Json => write!(f, "json"),
32            Self::Csv => write!(f, "csv"),
33        }
34    }
35}
36
37impl std::str::FromStr for OutputFormat {
38    type Err = crate::error::MktError;
39
40    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
41        match s.to_lowercase().as_str() {
42            "table" => Ok(Self::Table),
43            "json" => Ok(Self::Json),
44            "csv" => Ok(Self::Csv),
45            other => Err(crate::error::MktError::ValidationError {
46                field: "output".into(),
47                message: format!("Unknown output format: '{other}'. Use table, json, or csv."),
48            }),
49        }
50    }
51}
52
53/// Trait for types that can be rendered in table or CSV format.
54pub trait Formattable {
55    /// Column headers for table/CSV output.
56    fn headers() -> Vec<String>;
57    /// Row values corresponding to the headers.
58    fn row(&self) -> Vec<String>;
59}
60
61/// Format a list of items in the specified output format.
62///
63/// # Errors
64///
65/// Returns an error if JSON serialization or CSV writing fails.
66pub fn format_output<T: Formattable + Serialize>(
67    items: &[T],
68    format: OutputFormat,
69) -> Result<String> {
70    match format {
71        OutputFormat::Table => Ok(table::format_table::<T>(items)),
72        OutputFormat::Json => json::format_json(items),
73        OutputFormat::Csv => csv_fmt::format_csv::<T>(items),
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn output_format_display() {
83        assert_eq!(OutputFormat::Table.to_string(), "table");
84        assert_eq!(OutputFormat::Json.to_string(), "json");
85        assert_eq!(OutputFormat::Csv.to_string(), "csv");
86    }
87
88    #[test]
89    #[allow(clippy::expect_used)]
90    fn output_format_from_str() {
91        assert_eq!(
92            "table".parse::<OutputFormat>().expect("ok"),
93            OutputFormat::Table
94        );
95        assert_eq!(
96            "JSON".parse::<OutputFormat>().expect("ok"),
97            OutputFormat::Json
98        );
99        assert_eq!(
100            "csv".parse::<OutputFormat>().expect("ok"),
101            OutputFormat::Csv
102        );
103    }
104
105    #[test]
106    fn output_format_from_str_invalid() {
107        assert!("xml".parse::<OutputFormat>().is_err());
108    }
109}