1use anyhow::Result;
2use clap::Command;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6use crate::MetaPlugin;
7
8pub trait BasePlugin: MetaPlugin {
10 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 fn version(&self) -> Option<&str> {
23 None
24 }
25
26 fn description(&self) -> Option<&str> {
28 None
29 }
30
31 fn author(&self) -> Option<&str> {
33 None
34 }
35
36 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 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 fn show_ai_help(&self) -> Result<()> {
52 self.show_help(HelpFormat::Json)
53 }
54}
55
56#[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#[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
107pub trait HelpFormatter {
109 fn format_help(&self, app: &Command) -> Result<()>;
110}
111
112pub 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
124pub 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
136pub 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
148pub struct MarkdownHelpFormatter;
150
151impl HelpFormatter for MarkdownHelpFormatter {
152 fn format_help(&self, app: &Command) -> Result<()> {
153 let mut output = String::new();
154
155 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 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 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 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#[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 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 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 }