Skip to main content

turbovault_tools/
output_formatter.rs

1//! Output formatting for different transport types
2//!
3//! Provides human-readable, JSON, and text output formats for HTTP/WebSocket/TCP transports.
4//! STDIO transport always uses JSON per MCP protocol specification.
5
6use serde_json::Value;
7use std::fmt;
8use std::str::FromStr;
9
10/// Output format preference for HTTP/WebSocket/TCP transports
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum OutputFormat {
13    /// JSON format (default, also required for STDIO transport)
14    #[default]
15    Json,
16    /// Human-readable format with pretty-printed output
17    Human,
18    /// Plain text format for terminal output
19    Text,
20}
21
22impl FromStr for OutputFormat {
23    type Err = String;
24
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        match s.to_lowercase().as_str() {
27            "json" => Ok(OutputFormat::Json),
28            "human" => Ok(OutputFormat::Human),
29            "text" => Ok(OutputFormat::Text),
30            _ => Err(format!(
31                "Unknown output format '{}'. Valid options: json, human, text",
32                s
33            )),
34        }
35    }
36}
37
38impl fmt::Display for OutputFormat {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            OutputFormat::Json => write!(f, "json"),
42            OutputFormat::Human => write!(f, "human"),
43            OutputFormat::Text => write!(f, "text"),
44        }
45    }
46}
47
48/// Formatter for converting responses to different formats
49pub struct ResponseFormatter;
50
51impl ResponseFormatter {
52    /// Format a JSON response according to the output format preference
53    pub fn format(response: &Value, format: OutputFormat) -> String {
54        match format {
55            OutputFormat::Json => Self::format_json(response),
56            OutputFormat::Human => Self::format_human(response),
57            OutputFormat::Text => Self::format_text(response),
58        }
59    }
60
61    /// Format as JSON (pretty-printed)
62    fn format_json(response: &Value) -> String {
63        serde_json::to_string_pretty(response).unwrap_or_else(|_| response.to_string())
64    }
65
66    /// Format as human-readable output
67    fn format_human(response: &Value) -> String {
68        let mut output = String::new();
69
70        // Extract key information from standard response structure
71        if let Some(obj) = response.as_object() {
72            // Vault name
73            if let Some(vault) = obj.get("vault").and_then(|v| v.as_str()) {
74                output.push_str(&format!("šŸ“¦ Vault: {}\n", vault));
75            }
76
77            // Operation
78            if let Some(op) = obj.get("operation").and_then(|v| v.as_str()) {
79                output.push_str(&format!("āš™ļø  Operation: {}\n", op));
80            }
81
82            // Success indicator
83            if let Some(success) = obj.get("success").and_then(|v| v.as_bool()) {
84                let status = if success { "āœ… Success" } else { "āŒ Failed" };
85                output.push_str(&format!("Status: {}\n", status));
86            }
87
88            output.push('\n');
89
90            // Data section
91            if let Some(data) = obj.get("data") {
92                output.push_str("šŸ“Š Data:\n");
93                output.push_str(&Self::format_value_indented(data, 2));
94            }
95
96            // Warnings
97            if let Some(warnings) = obj.get("warnings").and_then(|v| v.as_array())
98                && !warnings.is_empty()
99            {
100                output.push_str("\nāš ļø  Warnings:\n");
101                for warning in warnings {
102                    if let Some(msg) = warning.as_str() {
103                        output.push_str(&format!("  • {}\n", msg));
104                    }
105                }
106            }
107
108            // Next steps
109            if let Some(steps) = obj.get("next_steps").and_then(|v| v.as_array())
110                && !steps.is_empty()
111            {
112                output.push_str("\nšŸ‘‰ Next Steps:\n");
113                for (i, step) in steps.iter().enumerate() {
114                    if let Some(s) = step.as_str() {
115                        output.push_str(&format!("  {}. {}\n", i + 1, s));
116                    }
117                }
118            }
119
120            // Performance metric
121            if let Some(took) = obj.get("took_ms").and_then(|v| v.as_u64()) {
122                output.push_str(&format!("\nā±ļø  Took: {}ms\n", took));
123            }
124
125            // Count if present
126            if let Some(count) = obj.get("count").and_then(|v| v.as_u64()) {
127                output.push_str(&format!("Count: {}\n", count));
128            }
129        }
130
131        if output.is_empty() {
132            Self::format_json(response)
133        } else {
134            output
135        }
136    }
137
138    /// Format as plain text
139    fn format_text(response: &Value) -> String {
140        let mut output = String::new();
141
142        if let Some(obj) = response.as_object() {
143            // Minimal output - just the key facts
144            if let Some(success) = obj.get("success").and_then(|v| v.as_bool()) {
145                output.push_str(if success {
146                    "āœ“ Success\n"
147                } else {
148                    "āœ— Failed\n"
149                });
150            }
151
152            if let Some(op) = obj.get("operation").and_then(|v| v.as_str()) {
153                output.push_str(&format!("{}\n", op));
154            }
155
156            // Brief data summary
157            if let Some(data) = obj.get("data") {
158                match data {
159                    Value::Object(map) => {
160                        for (key, value) in map.iter().take(5) {
161                            output.push_str(&format!("{}: ", key));
162                            match value {
163                                Value::String(s) => output.push_str(&format!("{}\n", s)),
164                                Value::Number(n) => output.push_str(&format!("{}\n", n)),
165                                Value::Bool(b) => output.push_str(&format!("{}\n", b)),
166                                Value::Array(arr) => {
167                                    output.push_str(&format!("[{} items]\n", arr.len()))
168                                }
169                                _ => output.push_str("...\n"),
170                            }
171                        }
172                    }
173                    Value::Array(arr) => {
174                        output.push_str(&format!("[{} items]\n", arr.len()));
175                    }
176                    Value::String(s) => output.push_str(&format!("{}\n", s)),
177                    _ => {}
178                }
179            }
180
181            if let Some(took) = obj.get("took_ms").and_then(|v| v.as_u64()) {
182                output.push_str(&format!("({} ms)\n", took));
183            }
184        }
185
186        if output.is_empty() {
187            Self::format_json(response)
188        } else {
189            output
190        }
191    }
192
193    /// Helper to format a value with indentation
194    fn format_value_indented(value: &Value, indent: usize) -> String {
195        let indent_str = " ".repeat(indent);
196
197        match value {
198            Value::Object(map) => {
199                let mut result = String::new();
200                for (key, val) in map.iter() {
201                    result.push_str(&format!("{}{}: ", indent_str, key));
202                    match val {
203                        Value::String(s) => result.push_str(&format!("{}\n", s)),
204                        Value::Number(n) => result.push_str(&format!("{}\n", n)),
205                        Value::Bool(b) => result.push_str(&format!("{}\n", b)),
206                        Value::Array(arr) => {
207                            result.push_str(&format!("[{} items]\n", arr.len()));
208                            for (i, item) in arr.iter().take(3).enumerate() {
209                                result.push_str(&format!("{}  [{}] {}\n", indent_str, i, item));
210                            }
211                            if arr.len() > 3 {
212                                result.push_str(&format!(
213                                    "{}  ... and {} more\n",
214                                    indent_str,
215                                    arr.len() - 3
216                                ));
217                            }
218                        }
219                        Value::Object(_) => {
220                            result.push_str(&format!(
221                                "{}\n",
222                                Self::format_value_indented(val, indent + 2)
223                            ));
224                        }
225                        Value::Null => result.push_str("null\n"),
226                    }
227                }
228                result
229            }
230            Value::Array(arr) => {
231                let mut result = String::from("[\n");
232                for (i, item) in arr.iter().take(5).enumerate() {
233                    result.push_str(&format!("{}  [{}] {}\n", indent_str, i, item));
234                }
235                if arr.len() > 5 {
236                    result.push_str(&format!("{}  ... and {} more\n", indent_str, arr.len() - 5));
237                }
238                result.push_str(&format!("{}]\n", indent_str));
239                result
240            }
241            Value::String(s) => format!("{}\n", s),
242            other => format!("{}\n", other),
243        }
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use serde_json::json;
251
252    #[test]
253    fn test_output_format_parse() {
254        assert_eq!(OutputFormat::from_str("json").unwrap(), OutputFormat::Json);
255        assert_eq!(OutputFormat::from_str("JSON").unwrap(), OutputFormat::Json);
256        assert_eq!(
257            OutputFormat::from_str("human").unwrap(),
258            OutputFormat::Human
259        );
260        assert_eq!(OutputFormat::from_str("text").unwrap(), OutputFormat::Text);
261        assert!(OutputFormat::from_str("invalid").is_err());
262    }
263
264    #[test]
265    fn test_format_json() {
266        let response = json!({
267            "success": true,
268            "data": {"test": "value"}
269        });
270        let formatted = ResponseFormatter::format(&response, OutputFormat::Json);
271        assert!(formatted.contains("\"success\": true"));
272    }
273
274    #[test]
275    fn test_format_human() {
276        let response = json!({
277            "vault": "personal",
278            "operation": "read_note",
279            "success": true,
280            "data": {"content": "test"},
281            "took_ms": 42
282        });
283        let formatted = ResponseFormatter::format(&response, OutputFormat::Human);
284        assert!(formatted.contains("Vault: personal"));
285        assert!(formatted.contains("āœ… Success"));
286        assert!(formatted.contains("42ms"));
287    }
288
289    #[test]
290    fn test_format_text() {
291        let response = json!({
292            "success": true,
293            "operation": "search",
294            "data": [1, 2, 3]
295        });
296        let formatted = ResponseFormatter::format(&response, OutputFormat::Text);
297        assert!(formatted.contains("āœ“ Success"));
298        assert!(formatted.contains("search"));
299    }
300}