mcp_execution_cli/
formatters.rs1use anyhow::Result;
6use colored::Colorize;
7use mcp_execution_core::cli::OutputFormat;
8use serde::Serialize;
9
10pub 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
51pub mod json {
53 use super::{Result, Serialize};
54
55 pub fn format<T: Serialize>(data: &T) -> Result<String> {
64 let json = serde_json::to_string_pretty(data)?;
65 Ok(json)
66 }
67
68 pub fn format_compact<T: Serialize>(data: &T) -> Result<String> {
75 let json = serde_json::to_string(data)?;
76 Ok(json)
77 }
78}
79
80pub mod text {
82 use super::{Result, Serialize, json};
83
84 pub fn format<T: Serialize>(data: &T) -> Result<String> {
94 json::format_compact(data)
96 }
97}
98
99pub mod pretty {
101 use super::{Colorize, Result, Serialize};
102
103 pub fn format<T: Serialize>(data: &T) -> Result<String> {
112 let value = serde_json::to_value(data)?;
114
115 format_value(&value, 0)
117 }
118
119 fn format_value(value: &serde_json::Value, indent: usize) -> Result<String> {
121 use serde_json::Value;
122
123 let indent_str = " ".repeat(indent);
124 let next_indent_str = " ".repeat(indent + 1);
125
126 match value {
127 Value::Null => Ok("null".dimmed().to_string()),
128 Value::Bool(b) => Ok(b.to_string().yellow().to_string()),
129 Value::Number(n) => Ok(n.to_string().cyan().to_string()),
130 Value::String(s) => {
131 let quoted = serde_json::to_string(s)?;
132 Ok(quoted.green().to_string())
133 }
134 Value::Array(arr) => {
135 if arr.is_empty() {
136 return Ok("[]".to_string());
137 }
138
139 let mut result = "[\n".to_string();
140 for (i, item) in arr.iter().enumerate() {
141 result.push_str(&next_indent_str);
142 result.push_str(&format_value(item, indent + 1)?);
143 if i < arr.len() - 1 {
144 result.push(',');
145 }
146 result.push('\n');
147 }
148 result.push_str(&indent_str);
149 result.push(']');
150 Ok(result)
151 }
152 Value::Object(obj) => {
153 if obj.is_empty() {
154 return Ok("{}".to_string());
155 }
156
157 let mut result = "{\n".to_string();
158 let entries: Vec<_> = obj.iter().collect();
159 for (i, (key, val)) in entries.iter().enumerate() {
160 result.push_str(&next_indent_str);
161 let quoted_key = serde_json::to_string(key)?;
162 result.push_str("ed_key.blue().bold().to_string());
163 result.push_str(": ");
164 result.push_str(&format_value(val, indent + 1)?);
165 if i < entries.len() - 1 {
166 result.push(',');
167 }
168 result.push('\n');
169 }
170 result.push_str(&indent_str);
171 result.push('}');
172 Ok(result)
173 }
174 }
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use serde::Serialize;
182
183 #[derive(Serialize)]
184 struct TestData {
185 name: String,
186 count: i32,
187 enabled: bool,
188 }
189
190 #[test]
191 fn test_json_format() {
192 let data = TestData {
193 name: "test".to_string(),
194 count: 42,
195 enabled: true,
196 };
197
198 let output = json::format(&data).unwrap();
199 assert!(output.contains("\"name\""));
200 assert!(output.contains("\"test\""));
201 assert!(output.contains("\"count\""));
202 assert!(output.contains("42"));
203 assert!(output.contains("\"enabled\""));
204 assert!(output.contains("true"));
205 }
206
207 #[test]
208 fn test_json_format_compact() {
209 let data = TestData {
210 name: "test".to_string(),
211 count: 42,
212 enabled: true,
213 };
214
215 let output = json::format_compact(&data).unwrap();
216 assert!(!output.contains('\n'));
218 assert!(output.contains("\"name\":\"test\""));
219 }
220
221 #[test]
222 fn test_text_format() {
223 let data = TestData {
224 name: "test".to_string(),
225 count: 42,
226 enabled: true,
227 };
228
229 let output = text::format(&data).unwrap();
230 assert!(!output.contains('\n'));
232 assert!(output.contains("\"name\":\"test\""));
233 }
234
235 #[test]
236 fn test_pretty_format() {
237 let data = TestData {
238 name: "test".to_string(),
239 count: 42,
240 enabled: true,
241 };
242
243 let output = pretty::format(&data).unwrap();
244 assert!(output.contains("name"));
246 assert!(output.contains("test"));
247 assert!(output.contains("count"));
248 assert!(output.contains("42"));
249 }
250
251 #[test]
252 fn test_format_output_json() {
253 let data = TestData {
254 name: "test".to_string(),
255 count: 42,
256 enabled: true,
257 };
258
259 let output = format_output(&data, OutputFormat::Json).unwrap();
260 assert!(output.contains("\"name\""));
261 }
262
263 #[test]
264 fn test_format_output_text() {
265 let data = TestData {
266 name: "test".to_string(),
267 count: 42,
268 enabled: true,
269 };
270
271 let output = format_output(&data, OutputFormat::Text).unwrap();
272 assert!(output.contains("\"name\""));
273 }
274
275 #[test]
276 fn test_pretty_format_escapes_quotes_and_newlines() {
277 #[derive(Serialize)]
281 struct Message {
282 text: String,
283 }
284
285 let data = Message {
286 text: "line one\nline \"two\" with \\backslash\\".to_string(),
287 };
288
289 let output = pretty::format(&data).unwrap();
290 let stripped = strip_ansi(&output);
291
292 let parsed: serde_json::Value = serde_json::from_str(&stripped).unwrap();
293 assert_eq!(parsed["text"], "line one\nline \"two\" with \\backslash\\");
294 }
295
296 #[test]
297 fn test_pretty_format_escapes_object_keys() {
298 let mut data = std::collections::BTreeMap::new();
303 data.insert("line one\nline \"two\" with \\backslash\\".to_string(), 1);
304
305 let output = pretty::format(&data).unwrap();
306 let stripped = strip_ansi(&output);
307
308 let parsed: serde_json::Value = serde_json::from_str(&stripped).unwrap();
309 assert_eq!(
310 parsed["line one\nline \"two\" with \\backslash\\"],
311 serde_json::json!(1)
312 );
313 }
314
315 fn strip_ansi(s: &str) -> String {
317 let mut result = String::with_capacity(s.len());
318 let mut chars = s.chars();
319 while let Some(c) = chars.next() {
320 if c == '\u{1b}' {
321 for c in chars.by_ref() {
322 if c == 'm' {
323 break;
324 }
325 }
326 } else {
327 result.push(c);
328 }
329 }
330 result
331 }
332
333 #[test]
334 fn test_format_output_pretty() {
335 let data = TestData {
336 name: "test".to_string(),
337 count: 42,
338 enabled: true,
339 };
340
341 let output = format_output(&data, OutputFormat::Pretty).unwrap();
342 assert!(output.contains("name"));
343 }
344}