turbomcp_cli/
formatter.rs

1//! Rich output formatting for CLI
2
3use crate::cli::OutputFormat;
4use crate::error::{CliError, CliResult};
5use comfy_table::{Table, modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL};
6use owo_colors::OwoColorize;
7use serde::Serialize;
8use turbomcp_protocol::types::*;
9
10/// Format and display output based on format preference
11pub struct Formatter {
12    format: OutputFormat,
13    colored: bool,
14}
15
16impl Formatter {
17    #[must_use]
18    pub fn new(format: OutputFormat, colored: bool) -> Self {
19        Self { format, colored }
20    }
21
22    /// Display any serializable value
23    pub fn display<T: Serialize + ?Sized>(&self, value: &T) -> CliResult<()> {
24        match self.format {
25            OutputFormat::Human => self.display_human(value),
26            OutputFormat::Json => self.display_json(value, true),
27            OutputFormat::Compact => self.display_json(value, false),
28            OutputFormat::Yaml => self.display_yaml(value),
29            OutputFormat::Table => self.display_human(value), // fallback to human
30        }
31    }
32
33    /// Display tools list
34    pub fn display_tools(&self, tools: &[Tool]) -> CliResult<()> {
35        match self.format {
36            OutputFormat::Human => {
37                if tools.is_empty() {
38                    self.print_info("No tools available");
39                    return Ok(());
40                }
41
42                self.print_header("Available Tools");
43                for tool in tools {
44                    self.print_tool(tool);
45                }
46                self.print_footer(&format!("Total: {} tools", tools.len()));
47                Ok(())
48            }
49            OutputFormat::Table => {
50                let mut table = Table::new();
51                table
52                    .load_preset(UTF8_FULL)
53                    .apply_modifier(UTF8_ROUND_CORNERS)
54                    .set_header(vec!["Name", "Description", "Input Schema"]);
55
56                for tool in tools {
57                    let schema_summary = format_schema_summary(&tool.input_schema);
58
59                    table.add_row(vec![
60                        &tool.name,
61                        tool.description.as_deref().unwrap_or("-"),
62                        &schema_summary,
63                    ]);
64                }
65
66                println!("{table}");
67                Ok(())
68            }
69            _ => self.display(tools),
70        }
71    }
72
73    /// Display resources list
74    pub fn display_resources(&self, resources: &[Resource]) -> CliResult<()> {
75        match self.format {
76            OutputFormat::Human => {
77                if resources.is_empty() {
78                    self.print_info("No resources available");
79                    return Ok(());
80                }
81
82                self.print_header("Available Resources");
83                for resource in resources {
84                    self.print_resource(resource);
85                }
86                self.print_footer(&format!("Total: {} resources", resources.len()));
87                Ok(())
88            }
89            OutputFormat::Table => {
90                let mut table = Table::new();
91                table
92                    .load_preset(UTF8_FULL)
93                    .apply_modifier(UTF8_ROUND_CORNERS)
94                    .set_header(vec!["URI", "Name", "Description", "MIME Type"]);
95
96                for resource in resources {
97                    let mime_str = resource
98                        .mime_type
99                        .as_ref()
100                        .map(|m| m.as_str())
101                        .unwrap_or("-");
102
103                    table.add_row(vec![
104                        resource.uri.as_str(),
105                        &resource.name,
106                        resource.description.as_deref().unwrap_or("-"),
107                        mime_str,
108                    ]);
109                }
110
111                println!("{table}");
112                Ok(())
113            }
114            _ => self.display(resources),
115        }
116    }
117
118    /// Display prompts list
119    pub fn display_prompts(&self, prompts: &[Prompt]) -> CliResult<()> {
120        match self.format {
121            OutputFormat::Human => {
122                if prompts.is_empty() {
123                    self.print_info("No prompts available");
124                    return Ok(());
125                }
126
127                self.print_header("Available Prompts");
128                for prompt in prompts {
129                    self.print_prompt(prompt);
130                }
131                self.print_footer(&format!("Total: {} prompts", prompts.len()));
132                Ok(())
133            }
134            OutputFormat::Table => {
135                let mut table = Table::new();
136                table
137                    .load_preset(UTF8_FULL)
138                    .apply_modifier(UTF8_ROUND_CORNERS)
139                    .set_header(vec!["Name", "Description", "Arguments"]);
140
141                for prompt in prompts {
142                    let args = prompt
143                        .arguments
144                        .as_ref()
145                        .map(|a| {
146                            a.iter()
147                                .map(|arg| arg.name.as_str())
148                                .collect::<Vec<_>>()
149                                .join(", ")
150                        })
151                        .unwrap_or_else(|| "None".to_string());
152
153                    table.add_row(vec![
154                        &prompt.name,
155                        prompt.description.as_deref().unwrap_or("-"),
156                        &args,
157                    ]);
158                }
159
160                println!("{table}");
161                Ok(())
162            }
163            _ => self.display(prompts),
164        }
165    }
166
167    /// Display server info
168    pub fn display_server_info(&self, info: &Implementation) -> CliResult<()> {
169        match self.format {
170            OutputFormat::Human => {
171                self.print_header("Server Information");
172                self.print_kv("Name", &info.name);
173                self.print_kv("Version", &info.version);
174                Ok(())
175            }
176            _ => self.display(info),
177        }
178    }
179
180    /// Display error with suggestions
181    pub fn display_error(&self, error: &CliError) {
182        if self.colored {
183            eprintln!("{}: {}", "Error".bright_red().bold(), error);
184
185            let suggestions = error.suggestions();
186            if !suggestions.is_empty() {
187                eprintln!("\n{}", "Suggestions:".bright_yellow().bold());
188                for suggestion in suggestions {
189                    eprintln!("  {} {}", "•".bright_blue(), suggestion);
190                }
191            }
192        } else {
193            eprintln!("Error: {error}");
194
195            let suggestions = error.suggestions();
196            if !suggestions.is_empty() {
197                eprintln!("\nSuggestions:");
198                for suggestion in suggestions {
199                    eprintln!("  • {suggestion}");
200                }
201            }
202        }
203    }
204
205    // Internal formatting helpers
206
207    fn display_json<T: Serialize + ?Sized>(&self, value: &T, pretty: bool) -> CliResult<()> {
208        let json = if pretty {
209            serde_json::to_string_pretty(value)?
210        } else {
211            serde_json::to_string(value)?
212        };
213        println!("{json}");
214        Ok(())
215    }
216
217    fn display_yaml<T: Serialize + ?Sized>(&self, value: &T) -> CliResult<()> {
218        let yaml = serde_yaml::to_string(value)?;
219        println!("{yaml}");
220        Ok(())
221    }
222
223    fn display_human<T: Serialize + ?Sized>(&self, value: &T) -> CliResult<()> {
224        // Fallback to pretty JSON for generic types
225        self.display_json(value, true)
226    }
227
228    fn print_header(&self, text: &str) {
229        if self.colored {
230            println!("\n{}", text.bright_cyan().bold());
231            println!("{}", "=".repeat(text.len()).bright_cyan());
232        } else {
233            println!("\n{text}");
234            println!("{}", "=".repeat(text.len()));
235        }
236    }
237
238    fn print_footer(&self, text: &str) {
239        if self.colored {
240            println!("\n{}", text.bright_black());
241        } else {
242            println!("\n{text}");
243        }
244    }
245
246    fn print_info(&self, text: &str) {
247        if self.colored {
248            println!("{}", text.bright_blue());
249        } else {
250            println!("{text}");
251        }
252    }
253
254    fn print_kv(&self, key: &str, value: &str) {
255        if self.colored {
256            println!("  {}: {}", key.bright_green().bold(), value);
257        } else {
258            println!("  {key}: {value}");
259        }
260    }
261
262    fn print_tool(&self, tool: &Tool) {
263        if self.colored {
264            println!(
265                "  {} {}",
266                "•".bright_blue(),
267                tool.name.bright_green().bold()
268            );
269            if let Some(desc) = &tool.description {
270                println!("    {desc}");
271            }
272        } else {
273            println!("  • {}", tool.name);
274            if let Some(desc) = &tool.description {
275                println!("    {desc}");
276            }
277        }
278    }
279
280    fn print_resource(&self, resource: &Resource) {
281        if self.colored {
282            println!(
283                "  {} {}",
284                "•".bright_blue(),
285                resource.uri.as_str().bright_green().bold()
286            );
287            println!("    Name: {}", resource.name);
288            if let Some(desc) = &resource.description {
289                println!("    {desc}");
290            }
291        } else {
292            println!("  • {}", resource.uri.as_str());
293            println!("    Name: {}", resource.name);
294            if let Some(desc) = &resource.description {
295                println!("    {desc}");
296            }
297        }
298    }
299
300    fn print_prompt(&self, prompt: &Prompt) {
301        if self.colored {
302            println!(
303                "  {} {}",
304                "•".bright_blue(),
305                prompt.name.bright_green().bold()
306            );
307            if let Some(desc) = &prompt.description {
308                println!("    {desc}");
309            }
310            if let Some(args) = &prompt.arguments {
311                if !args.is_empty() {
312                    let arg_names: Vec<_> = args.iter().map(|a| a.name.as_str()).collect();
313                    println!("    Arguments: {}", arg_names.join(", ").bright_yellow());
314                }
315            }
316        } else {
317            println!("  • {}", prompt.name);
318            if let Some(desc) = &prompt.description {
319                println!("    {desc}");
320            }
321            if let Some(args) = &prompt.arguments {
322                if !args.is_empty() {
323                    let arg_names: Vec<_> = args.iter().map(|a| a.name.as_str()).collect();
324                    println!("    Arguments: {}", arg_names.join(", "));
325                }
326            }
327        }
328    }
329}
330
331/// Format schema summary for table display
332fn format_schema_summary(schema: &ToolInputSchema) -> String {
333    if let Some(props) = &schema.properties {
334        if !props.is_empty() {
335            let prop_names: Vec<_> = props.keys().map(|k| k.as_str()).collect();
336            return prop_names.join(", ");
337        }
338    }
339    "No properties".to_string()
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_formatter_creation() {
348        let formatter = Formatter::new(OutputFormat::Human, true);
349        assert!(formatter.colored);
350    }
351}