Skip to main content

metarepo_core/
plugin_base.rs

1use anyhow::Result;
2use clap::Command;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6use crate::MetaPlugin;
7
8/// Base trait for plugins with default implementations
9pub trait BasePlugin: MetaPlugin {
10    /// Get plugin metadata
11    fn metadata(&self) -> PluginMetadata {
12        PluginMetadata {
13            name: self.name().to_string(),
14            version: self.version().unwrap_or("0.1.0").to_string(),
15            description: self.description().unwrap_or("").to_string(),
16            author: self.author().unwrap_or("").to_string(),
17            experimental: self.is_experimental(),
18        }
19    }
20
21    /// Get plugin version
22    fn version(&self) -> Option<&str> {
23        None
24    }
25
26    /// Get plugin description
27    fn description(&self) -> Option<&str> {
28        None
29    }
30
31    /// Get plugin author
32    fn author(&self) -> Option<&str> {
33        None
34    }
35
36    /// Default help implementation that generates from command structure
37    fn show_help(&self, format: HelpFormat) -> Result<()> {
38        let app = self.build_help_command();
39        let formatter = format.formatter();
40        formatter.format_help(&app)
41    }
42
43    /// Build a command for help generation
44    fn build_help_command(&self) -> Command {
45        let name: &'static str = Box::leak(format!("meta {}", self.name()).into_boxed_str());
46        let app = Command::new(name);
47        self.register_commands(app)
48    }
49
50    /// Show AI-friendly help output
51    fn show_ai_help(&self) -> Result<()> {
52        self.show_help(HelpFormat::Json)
53    }
54}
55
56/// Plugin metadata structure
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct PluginMetadata {
59    pub name: String,
60    pub version: String,
61    pub description: String,
62    pub author: String,
63    pub experimental: bool,
64}
65
66/// Help output format
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum HelpFormat {
69    Terminal,
70    Json,
71    Yaml,
72    Markdown,
73}
74
75impl HelpFormat {
76    pub fn formatter(&self) -> Box<dyn HelpFormatter> {
77        match self {
78            HelpFormat::Terminal => Box::new(TerminalHelpFormatter),
79            HelpFormat::Json => Box::new(JsonHelpFormatter),
80            HelpFormat::Yaml => Box::new(YamlHelpFormatter),
81            HelpFormat::Markdown => Box::new(MarkdownHelpFormatter),
82        }
83    }
84
85    pub fn parse(s: &str) -> Option<Self> {
86        match s.to_lowercase().as_str() {
87            "terminal" | "term" => Some(HelpFormat::Terminal),
88            "json" => Some(HelpFormat::Json),
89            "yaml" | "yml" => Some(HelpFormat::Yaml),
90            "markdown" | "md" => Some(HelpFormat::Markdown),
91            _ => None,
92        }
93    }
94}
95
96impl fmt::Display for HelpFormat {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            HelpFormat::Terminal => write!(f, "terminal"),
100            HelpFormat::Json => write!(f, "json"),
101            HelpFormat::Yaml => write!(f, "yaml"),
102            HelpFormat::Markdown => write!(f, "markdown"),
103        }
104    }
105}
106
107/// Trait for formatting help output
108pub trait HelpFormatter {
109    fn format_help(&self, app: &Command) -> Result<()>;
110}
111
112/// Terminal help formatter (default colorful output)
113pub struct TerminalHelpFormatter;
114
115impl HelpFormatter for TerminalHelpFormatter {
116    fn format_help(&self, app: &Command) -> Result<()> {
117        let mut app = app.clone();
118        app.print_help()?;
119        println!();
120        Ok(())
121    }
122}
123
124/// JSON help formatter for structured output
125pub struct JsonHelpFormatter;
126
127impl HelpFormatter for JsonHelpFormatter {
128    fn format_help(&self, app: &Command) -> Result<()> {
129        let help_data = extract_command_info(app);
130        let json = serde_json::to_string_pretty(&help_data)?;
131        println!("{}", json);
132        Ok(())
133    }
134}
135
136/// YAML help formatter for structured output
137pub struct YamlHelpFormatter;
138
139impl HelpFormatter for YamlHelpFormatter {
140    fn format_help(&self, app: &Command) -> Result<()> {
141        let help_data = extract_command_info(app);
142        let yaml = serde_yaml::to_string(&help_data)?;
143        println!("{}", yaml);
144        Ok(())
145    }
146}
147
148/// Markdown help formatter for documentation
149pub struct MarkdownHelpFormatter;
150
151impl HelpFormatter for MarkdownHelpFormatter {
152    fn format_help(&self, app: &Command) -> Result<()> {
153        let mut output = String::new();
154
155        // Command name and description
156        output.push_str(&format!("# {}\n\n", app.get_name()));
157        if let Some(about) = app.get_about() {
158            output.push_str(&format!("{}\n\n", about));
159        }
160
161        // Usage
162        output.push_str("## Usage\n\n```\n");
163        output.push_str(&format!("{} [OPTIONS]", app.get_name()));
164        if app.get_subcommands().count() > 0 {
165            output.push_str(" <COMMAND>");
166        }
167        output.push_str("\n```\n\n");
168
169        // Options
170        let args: Vec<_> = app.get_arguments().collect();
171        if !args.is_empty() {
172            output.push_str("## Options\n\n");
173            for arg in args {
174                if let Some(help) = arg.get_help() {
175                    let short = arg
176                        .get_short()
177                        .map(|s| format!("-{}", s))
178                        .unwrap_or_default();
179                    let long = arg
180                        .get_long()
181                        .map(|l| format!("--{}", l))
182                        .unwrap_or_default();
183                    let flags = match (&short[..], &long[..]) {
184                        ("", l) => l.to_string(),
185                        (s, "") => s.to_string(),
186                        (s, l) => format!("{}, {}", s, l),
187                    };
188                    output.push_str(&format!("- `{}`: {}\n", flags, help));
189                }
190            }
191            output.push('\n');
192        }
193
194        // Subcommands
195        let subcommands: Vec<_> = app.get_subcommands().collect();
196        if !subcommands.is_empty() {
197            output.push_str("## Commands\n\n");
198            for subcmd in subcommands {
199                output.push_str(&format!("### {}\n\n", subcmd.get_name()));
200                if let Some(about) = subcmd.get_about() {
201                    output.push_str(&format!("{}\n\n", about));
202                }
203            }
204        }
205
206        println!("{}", output);
207        Ok(())
208    }
209}
210
211/// Extract command information for structured output
212#[derive(Debug, Serialize, Deserialize)]
213pub struct CommandInfo {
214    pub name: String,
215    pub description: Option<String>,
216    pub version: Option<String>,
217    pub subcommands: Vec<CommandInfo>,
218    pub arguments: Vec<ArgumentInfo>,
219}
220
221#[derive(Debug, Serialize, Deserialize)]
222pub struct ArgumentInfo {
223    pub name: String,
224    pub short: Option<char>,
225    pub long: Option<String>,
226    pub help: Option<String>,
227    pub required: bool,
228    pub takes_value: bool,
229}
230
231fn extract_command_info(app: &Command) -> CommandInfo {
232    CommandInfo {
233        name: app.get_name().to_string(),
234        description: app.get_about().map(|s| s.to_string()),
235        version: app.get_version().map(|s| s.to_string()),
236        subcommands: app.get_subcommands().map(extract_command_info).collect(),
237        arguments: app
238            .get_arguments()
239            .map(|arg| ArgumentInfo {
240                name: arg.get_id().to_string(),
241                short: arg.get_short(),
242                long: arg.get_long().map(|s| s.to_string()),
243                help: arg.get_help().map(|s| s.to_string()),
244                required: arg.is_required_set(),
245                takes_value: arg
246                    .get_num_args()
247                    .map(|n| n.takes_values())
248                    .unwrap_or(false),
249            })
250            .collect(),
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_help_format_from_str() {
260        assert_eq!(HelpFormat::parse("terminal"), Some(HelpFormat::Terminal));
261        assert_eq!(HelpFormat::parse("term"), Some(HelpFormat::Terminal));
262        assert_eq!(HelpFormat::parse("json"), Some(HelpFormat::Json));
263        assert_eq!(HelpFormat::parse("yaml"), Some(HelpFormat::Yaml));
264        assert_eq!(HelpFormat::parse("yml"), Some(HelpFormat::Yaml));
265        assert_eq!(HelpFormat::parse("markdown"), Some(HelpFormat::Markdown));
266        assert_eq!(HelpFormat::parse("md"), Some(HelpFormat::Markdown));
267        assert_eq!(HelpFormat::parse("unknown"), None);
268
269        // Test case insensitive
270        assert_eq!(HelpFormat::parse("JSON"), Some(HelpFormat::Json));
271        assert_eq!(HelpFormat::parse("Terminal"), Some(HelpFormat::Terminal));
272    }
273
274    #[test]
275    fn test_help_format_display() {
276        assert_eq!(format!("{}", HelpFormat::Terminal), "terminal");
277        assert_eq!(format!("{}", HelpFormat::Json), "json");
278        assert_eq!(format!("{}", HelpFormat::Yaml), "yaml");
279        assert_eq!(format!("{}", HelpFormat::Markdown), "markdown");
280    }
281
282    #[test]
283    fn test_extract_command_info() {
284        let app = Command::new("test-app")
285            .version("1.0.0")
286            .about("Test application")
287            .arg(
288                clap::Arg::new("verbose")
289                    .short('v')
290                    .long("verbose")
291                    .help("Enable verbose output"),
292            )
293            .arg(
294                clap::Arg::new("input")
295                    .long("input")
296                    .help("Input file")
297                    .required(true)
298                    .value_name("FILE"),
299            )
300            .subcommand(
301                Command::new("sub")
302                    .about("Subcommand")
303                    .arg(clap::Arg::new("flag").short('f').help("A flag")),
304            );
305
306        let info = extract_command_info(&app);
307
308        assert_eq!(info.name, "test-app");
309        assert_eq!(info.description, Some("Test application".to_string()));
310        assert_eq!(info.version, Some("1.0.0".to_string()));
311        assert_eq!(info.subcommands.len(), 1);
312        assert_eq!(info.subcommands[0].name, "sub");
313
314        // Check arguments (note: clap includes help and version by default)
315        let verbose_arg = info.arguments.iter().find(|a| a.name == "verbose");
316        assert!(verbose_arg.is_some());
317        let verbose = verbose_arg.unwrap();
318        assert_eq!(verbose.short, Some('v'));
319        assert_eq!(verbose.long, Some("verbose".to_string()));
320        assert_eq!(verbose.help, Some("Enable verbose output".to_string()));
321
322        let input_arg = info.arguments.iter().find(|a| a.name == "input");
323        assert!(input_arg.is_some());
324        let input = input_arg.unwrap();
325        assert_eq!(input.long, Some("input".to_string()));
326        assert!(input.required);
327    }
328
329    #[test]
330    fn test_plugin_metadata() {
331        #[derive(Debug)]
332        struct TestPlugin;
333
334        impl MetaPlugin for TestPlugin {
335            fn name(&self) -> &str {
336                "test"
337            }
338
339            fn register_commands(&self, app: Command) -> Command {
340                app
341            }
342
343            fn handle_command(
344                &self,
345                _matches: &clap::ArgMatches,
346                _config: &crate::RuntimeConfig,
347            ) -> Result<()> {
348                Ok(())
349            }
350
351            fn is_experimental(&self) -> bool {
352                true
353            }
354        }
355
356        impl BasePlugin for TestPlugin {
357            fn version(&self) -> Option<&str> {
358                Some("1.2.3")
359            }
360
361            fn description(&self) -> Option<&str> {
362                Some("Test plugin")
363            }
364
365            fn author(&self) -> Option<&str> {
366                Some("Test Author")
367            }
368        }
369
370        let plugin = TestPlugin;
371        let metadata = plugin.metadata();
372
373        assert_eq!(metadata.name, "test");
374        assert_eq!(metadata.version, "1.2.3");
375        assert_eq!(metadata.description, "Test plugin");
376        assert_eq!(metadata.author, "Test Author");
377        assert!(metadata.experimental);
378    }
379
380    // Note: Testing formatter types is not directly possible due to trait object limitations
381    // The test above for HelpFormat::from_str and display is sufficient
382}