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