mcp_execution_cli/
formatters.rs

1//! Output formatters for CLI commands.
2//!
3//! Provides consistent formatting across all CLI commands for JSON, text, and pretty output modes.
4
5use anyhow::Result;
6use colored::Colorize;
7use mcp_execution_core::cli::OutputFormat;
8use serde::Serialize;
9
10/// Format data according to the specified output format.
11///
12/// # Arguments
13///
14/// * `data` - The data to format (must be serializable)
15/// * `format` - The output format (Json, Text, Pretty)
16///
17/// # Errors
18///
19/// Returns an error if JSON serialization fails.
20///
21/// # Examples
22///
23/// ```
24/// use mcp_execution_cli::formatters::format_output;
25/// use mcp_execution_core::cli::OutputFormat;
26/// use serde::Serialize;
27///
28/// #[derive(Serialize)]
29/// struct ServerInfo {
30///     name: String,
31///     version: String,
32/// }
33///
34/// let info = ServerInfo {
35///     name: "test-server".to_string(),
36///     version: "1.0.0".to_string(),
37/// };
38///
39/// let output = format_output(&info, OutputFormat::Json)?;
40/// assert!(output.contains("\"name\""));
41/// # Ok::<(), anyhow::Error>(())
42/// ```
43pub fn format_output<T: Serialize>(data: &T, format: OutputFormat) -> Result<String> {
44    match format {
45        OutputFormat::Json => json::format(data),
46        OutputFormat::Text => text::format(data),
47        OutputFormat::Pretty => pretty::format(data),
48    }
49}
50
51/// JSON output formatting.
52pub mod json {
53    use super::{Result, Serialize};
54
55    /// Format data as JSON.
56    ///
57    /// Uses pretty-printing with 2-space indentation.
58    pub fn format<T: Serialize>(data: &T) -> Result<String> {
59        let json = serde_json::to_string_pretty(data)?;
60        Ok(json)
61    }
62
63    /// Format data as compact JSON (no formatting).
64    pub fn format_compact<T: Serialize>(data: &T) -> Result<String> {
65        let json = serde_json::to_string(data)?;
66        Ok(json)
67    }
68}
69
70/// Plain text output formatting.
71pub mod text {
72    use super::{Result, Serialize, json};
73
74    /// Format data as plain text.
75    ///
76    /// Uses JSON representation but without colors or fancy formatting.
77    /// Suitable for piping to other commands or scripts.
78    pub fn format<T: Serialize>(data: &T) -> Result<String> {
79        // For text mode, use JSON without pretty printing
80        json::format_compact(data)
81    }
82}
83
84/// Pretty (human-readable) output formatting.
85pub mod pretty {
86    use super::{Colorize, Result, Serialize};
87
88    /// Format data as colorized, human-readable output.
89    ///
90    /// Uses colors and formatting for better terminal readability.
91    pub fn format<T: Serialize>(data: &T) -> Result<String> {
92        // Convert to JSON value first for inspection
93        let value = serde_json::to_value(data)?;
94
95        // Format with colors
96        format_value(&value, 0)
97    }
98
99    /// Recursively format a JSON value with colors and indentation.
100    fn format_value(value: &serde_json::Value, indent: usize) -> Result<String> {
101        use serde_json::Value;
102
103        let indent_str = "  ".repeat(indent);
104        let next_indent_str = "  ".repeat(indent + 1);
105
106        match value {
107            Value::Null => Ok("null".dimmed().to_string()),
108            Value::Bool(b) => Ok(b.to_string().yellow().to_string()),
109            Value::Number(n) => Ok(n.to_string().cyan().to_string()),
110            Value::String(s) => Ok(format!("\"{}\"", s.green())),
111            Value::Array(arr) => {
112                if arr.is_empty() {
113                    return Ok("[]".to_string());
114                }
115
116                let mut result = "[\n".to_string();
117                for (i, item) in arr.iter().enumerate() {
118                    result.push_str(&next_indent_str);
119                    result.push_str(&format_value(item, indent + 1)?);
120                    if i < arr.len() - 1 {
121                        result.push(',');
122                    }
123                    result.push('\n');
124                }
125                result.push_str(&indent_str);
126                result.push(']');
127                Ok(result)
128            }
129            Value::Object(obj) => {
130                if obj.is_empty() {
131                    return Ok("{}".to_string());
132                }
133
134                let mut result = "{\n".to_string();
135                let entries: Vec<_> = obj.iter().collect();
136                for (i, (key, val)) in entries.iter().enumerate() {
137                    result.push_str(&next_indent_str);
138                    result.push_str(&format!("\"{}\": ", key.blue().bold()));
139                    result.push_str(&format_value(val, indent + 1)?);
140                    if i < entries.len() - 1 {
141                        result.push(',');
142                    }
143                    result.push('\n');
144                }
145                result.push_str(&indent_str);
146                result.push('}');
147                Ok(result)
148            }
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use serde::Serialize;
157
158    #[derive(Serialize)]
159    struct TestData {
160        name: String,
161        count: i32,
162        enabled: bool,
163    }
164
165    #[test]
166    fn test_json_format() {
167        let data = TestData {
168            name: "test".to_string(),
169            count: 42,
170            enabled: true,
171        };
172
173        let output = json::format(&data).unwrap();
174        assert!(output.contains("\"name\""));
175        assert!(output.contains("\"test\""));
176        assert!(output.contains("\"count\""));
177        assert!(output.contains("42"));
178        assert!(output.contains("\"enabled\""));
179        assert!(output.contains("true"));
180    }
181
182    #[test]
183    fn test_json_format_compact() {
184        let data = TestData {
185            name: "test".to_string(),
186            count: 42,
187            enabled: true,
188        };
189
190        let output = json::format_compact(&data).unwrap();
191        // Compact format should not have newlines
192        assert!(!output.contains('\n'));
193        assert!(output.contains("\"name\":\"test\""));
194    }
195
196    #[test]
197    fn test_text_format() {
198        let data = TestData {
199            name: "test".to_string(),
200            count: 42,
201            enabled: true,
202        };
203
204        let output = text::format(&data).unwrap();
205        // Text format uses compact JSON
206        assert!(!output.contains('\n'));
207        assert!(output.contains("\"name\":\"test\""));
208    }
209
210    #[test]
211    fn test_pretty_format() {
212        let data = TestData {
213            name: "test".to_string(),
214            count: 42,
215            enabled: true,
216        };
217
218        let output = pretty::format(&data).unwrap();
219        // Pretty format should have structure
220        assert!(output.contains("name"));
221        assert!(output.contains("test"));
222        assert!(output.contains("count"));
223        assert!(output.contains("42"));
224    }
225
226    #[test]
227    fn test_format_output_json() {
228        let data = TestData {
229            name: "test".to_string(),
230            count: 42,
231            enabled: true,
232        };
233
234        let output = format_output(&data, OutputFormat::Json).unwrap();
235        assert!(output.contains("\"name\""));
236    }
237
238    #[test]
239    fn test_format_output_text() {
240        let data = TestData {
241            name: "test".to_string(),
242            count: 42,
243            enabled: true,
244        };
245
246        let output = format_output(&data, OutputFormat::Text).unwrap();
247        assert!(output.contains("\"name\""));
248    }
249
250    #[test]
251    fn test_format_output_pretty() {
252        let data = TestData {
253            name: "test".to_string(),
254            count: 42,
255            enabled: true,
256        };
257
258        let output = format_output(&data, OutputFormat::Pretty).unwrap();
259        assert!(output.contains("name"));
260    }
261}