1use anyhow::Result;
2use clap::{Arg, ArgMatches, Command};
3use std::collections::HashMap;
4
5use crate::{MetaPlugin, RuntimeConfig, BasePlugin};
6
7pub struct PluginBuilder {
9 name: String,
10 version: String,
11 description: String,
12 author: String,
13 experimental: bool,
14 commands: Vec<CommandBuilder>,
15 handlers: HashMap<String, Box<dyn Fn(&ArgMatches, &RuntimeConfig) -> Result<()> + Send + Sync>>,
16}
17
18impl PluginBuilder {
19 pub fn new(name: impl Into<String>) -> Self {
21 Self {
22 name: name.into(),
23 version: "0.1.0".to_string(),
24 description: String::new(),
25 author: String::new(),
26 experimental: false,
27 commands: Vec::new(),
28 handlers: HashMap::new(),
29 }
30 }
31
32 pub fn version(mut self, version: impl Into<String>) -> Self {
34 self.version = version.into();
35 self
36 }
37
38 pub fn description(mut self, desc: impl Into<String>) -> Self {
40 self.description = desc.into();
41 self
42 }
43
44 pub fn author(mut self, author: impl Into<String>) -> Self {
46 self.author = author.into();
47 self
48 }
49
50 pub fn experimental(mut self, experimental: bool) -> Self {
52 self.experimental = experimental;
53 self
54 }
55
56 pub fn command(mut self, builder: CommandBuilder) -> Self {
58 self.commands.push(builder);
59 self
60 }
61
62 pub fn handler<F>(mut self, command: impl Into<String>, handler: F) -> Self
64 where
65 F: Fn(&ArgMatches, &RuntimeConfig) -> Result<()> + Send + Sync + 'static,
66 {
67 self.handlers.insert(command.into(), Box::new(handler));
68 self
69 }
70
71 pub fn build(self) -> BuiltPlugin {
73 BuiltPlugin {
74 name: self.name,
75 version: self.version,
76 description: self.description,
77 author: self.author,
78 experimental: self.experimental,
79 commands: self.commands,
80 handlers: self.handlers,
81 }
82 }
83}
84
85pub struct BuiltPlugin {
87 name: String,
88 version: String,
89 description: String,
90 author: String,
91 experimental: bool,
92 commands: Vec<CommandBuilder>,
93 handlers: HashMap<String, Box<dyn Fn(&ArgMatches, &RuntimeConfig) -> Result<()> + Send + Sync>>,
94}
95
96impl MetaPlugin for BuiltPlugin {
97 fn name(&self) -> &str {
98 &self.name
99 }
100
101 fn register_commands(&self, app: Command) -> Command {
102 if self.commands.is_empty() {
103 return app;
104 }
105
106 let name: &'static str = Box::leak(self.name.clone().into_boxed_str());
107 let desc: &'static str = Box::leak(self.description.clone().into_boxed_str());
108 let vers: &'static str = Box::leak(self.version.clone().into_boxed_str());
109
110 let mut plugin_cmd = Command::new(name)
111 .about(desc)
112 .version(vers);
113
114 for cmd_builder in &self.commands {
115 plugin_cmd = plugin_cmd.subcommand(cmd_builder.build());
116 }
117
118 app.subcommand(plugin_cmd)
119 }
120
121 fn handle_command(&self, matches: &ArgMatches, config: &RuntimeConfig) -> Result<()> {
122 if let Some((cmd_name, sub_matches)) = matches.subcommand() {
124 if let Some(handler) = self.handlers.get(cmd_name) {
126 return handler(sub_matches, config);
127 }
128
129 println!("No handler registered for command: {}", cmd_name);
131 }
132
133 let mut help_cmd = self.build_help_command();
135 help_cmd.print_help()?;
136 Ok(())
137 }
138
139 fn is_experimental(&self) -> bool {
140 self.experimental
141 }
142}
143
144impl BuiltPlugin {
145 fn build_help_command(&self) -> Command {
146 let name: &'static str = Box::leak(self.name.clone().into_boxed_str());
147 let desc: &'static str = Box::leak(self.description.clone().into_boxed_str());
148 let vers: &'static str = Box::leak(self.version.clone().into_boxed_str());
149
150 let mut plugin_cmd = Command::new(name)
151 .about(desc)
152 .version(vers);
153
154 for cmd_builder in &self.commands {
155 plugin_cmd = plugin_cmd.subcommand(cmd_builder.build());
156 }
157
158 plugin_cmd
159 }
160}
161
162impl BasePlugin for BuiltPlugin {
163 fn version(&self) -> Option<&str> {
164 Some(&self.version)
165 }
166
167 fn description(&self) -> Option<&str> {
168 Some(&self.description)
169 }
170
171 fn author(&self) -> Option<&str> {
172 Some(&self.author)
173 }
174}
175
176pub struct CommandBuilder {
178 name: String,
179 about: String,
180 long_about: Option<String>,
181 aliases: Vec<String>,
182 args: Vec<ArgBuilder>,
183 subcommands: Vec<CommandBuilder>,
184 allow_external_subcommands: bool,
185}
186
187impl CommandBuilder {
188 pub fn new(name: impl Into<String>) -> Self {
190 Self {
191 name: name.into(),
192 about: String::new(),
193 long_about: None,
194 aliases: Vec::new(),
195 args: Vec::new(),
196 subcommands: Vec::new(),
197 allow_external_subcommands: false,
198 }
199 }
200
201 pub fn about(mut self, about: impl Into<String>) -> Self {
203 self.about = about.into();
204 self
205 }
206
207 pub fn long_about(mut self, long_about: impl Into<String>) -> Self {
209 self.long_about = Some(long_about.into());
210 self
211 }
212
213 pub fn alias(mut self, alias: impl Into<String>) -> Self {
215 self.aliases.push(alias.into());
216 self
217 }
218
219 pub fn aliases(mut self, aliases: Vec<String>) -> Self {
221 self.aliases.extend(aliases);
222 self
223 }
224
225 pub fn arg(mut self, arg: ArgBuilder) -> Self {
227 self.args.push(arg);
228 self
229 }
230
231 pub fn subcommand(mut self, cmd: CommandBuilder) -> Self {
233 self.subcommands.push(cmd);
234 self
235 }
236
237 pub fn allow_external_subcommands(mut self, allow: bool) -> Self {
239 self.allow_external_subcommands = allow;
240 self
241 }
242
243 pub fn with_help_formatting(mut self) -> Self {
246 self.args.push(
248 ArgBuilder::new("output-format")
249 .long("output-format")
250 .help("Output format (json, yaml, markdown)")
251 .takes_value(true)
252 .possible_value("json")
253 .possible_value("yaml")
254 .possible_value("markdown")
255 );
256
257 self.args.push(
259 ArgBuilder::new("ai")
260 .long("ai")
261 .help("Show AI-friendly structured output (same as --output-format=json)")
262 .takes_value(false) );
264
265 self
266 }
267
268 fn build(&self) -> Command {
270 let name: &'static str = Box::leak(self.name.clone().into_boxed_str());
271 let about: &'static str = Box::leak(self.about.clone().into_boxed_str());
272
273 let mut cmd = Command::new(name)
274 .about(about)
275 .version(env!("CARGO_PKG_VERSION"));
276
277 if let Some(ref long_about) = self.long_about {
278 let long_about_str: &'static str = Box::leak(long_about.clone().into_boxed_str());
279 cmd = cmd.long_about(long_about_str);
280 }
281
282 if !self.aliases.is_empty() {
283 let aliases: Vec<&'static str> = self.aliases.iter()
284 .map(|s| Box::leak(s.clone().into_boxed_str()) as &'static str)
285 .collect();
286 cmd = cmd.visible_aliases(aliases);
287 }
288
289 for arg_builder in &self.args {
290 cmd = cmd.arg(arg_builder.build());
291 }
292
293 for subcmd_builder in &self.subcommands {
294 cmd = cmd.subcommand(subcmd_builder.build());
295 }
296
297 if self.allow_external_subcommands {
298 cmd = cmd.allow_external_subcommands(true);
299 }
300
301 cmd
302 }
303}
304
305pub struct ArgBuilder {
307 name: String,
308 short: Option<char>,
309 long: Option<String>,
310 help: Option<String>,
311 required: bool,
312 takes_value: bool,
313 default_value: Option<String>,
314 possible_values: Vec<String>,
315}
316
317impl ArgBuilder {
318 pub fn new(name: impl Into<String>) -> Self {
320 Self {
321 name: name.into(),
322 short: None,
323 long: None,
324 help: None,
325 required: false,
326 takes_value: false,
327 default_value: None,
328 possible_values: Vec::new(),
329 }
330 }
331
332 pub fn short(mut self, short: char) -> Self {
334 self.short = Some(short);
335 self
336 }
337
338 pub fn long(mut self, long: impl Into<String>) -> Self {
340 self.long = Some(long.into());
341 self
342 }
343
344 pub fn help(mut self, help: impl Into<String>) -> Self {
346 self.help = Some(help.into());
347 self
348 }
349
350 pub fn required(mut self, required: bool) -> Self {
352 self.required = required;
353 self
354 }
355
356 pub fn takes_value(mut self, takes: bool) -> Self {
358 self.takes_value = takes;
359 self
360 }
361
362 pub fn default_value(mut self, value: impl Into<String>) -> Self {
364 self.default_value = Some(value.into());
365 self
366 }
367
368 pub fn possible_value(mut self, value: impl Into<String>) -> Self {
370 self.possible_values.push(value.into());
371 self
372 }
373
374 fn build(&self) -> Arg {
376 let name: &'static str = Box::leak(self.name.clone().into_boxed_str());
377 let mut arg = Arg::new(name);
378
379 if let Some(short) = self.short {
380 arg = arg.short(short);
381 }
382
383 if let Some(ref long) = self.long {
384 let long_str: &'static str = Box::leak(long.clone().into_boxed_str());
385 arg = arg.long(long_str);
386 }
387
388 if let Some(ref help) = self.help {
389 let help_str: &'static str = Box::leak(help.clone().into_boxed_str());
390 arg = arg.help(help_str);
391 }
392
393 if self.required {
394 arg = arg.required(true);
395 }
396
397 if self.takes_value {
398 arg = arg.action(clap::ArgAction::Set);
399 } else {
400 arg = arg.action(clap::ArgAction::SetTrue);
401 }
402
403 if let Some(ref default) = self.default_value {
404 let default_str: &'static str = Box::leak(default.clone().into_boxed_str());
405 arg = arg.default_value(default_str);
406 }
407
408 if !self.possible_values.is_empty() {
409 let values: Vec<&'static str> = self.possible_values.iter()
410 .map(|s| Box::leak(s.clone().into_boxed_str()) as &'static str)
411 .collect();
412 arg = arg.value_parser(values);
413 }
414
415 arg
416 }
417}
418
419pub fn plugin(name: impl Into<String>) -> PluginBuilder {
421 PluginBuilder::new(name)
422}
423
424pub fn command(name: impl Into<String>) -> CommandBuilder {
426 CommandBuilder::new(name)
427}
428
429pub fn arg(name: impl Into<String>) -> ArgBuilder {
431 ArgBuilder::new(name)
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_plugin_builder_basic() {
440 let plugin = plugin("test-plugin")
441 .version("1.0.0")
442 .description("Test plugin")
443 .author("Test Author")
444 .experimental(true)
445 .build();
446
447 assert_eq!(plugin.name(), "test-plugin");
448 assert_eq!(plugin.version(), Some("1.0.0"));
449 assert_eq!(plugin.description(), Some("Test plugin"));
450 assert_eq!(plugin.author(), Some("Test Author"));
451 assert!(plugin.is_experimental());
452 }
453
454 #[test]
455 fn test_plugin_builder_with_commands() {
456 let test_handler = |_matches: &ArgMatches, _config: &RuntimeConfig| -> Result<()> {
457 Ok(())
458 };
459
460 let plugin = plugin("test-plugin")
461 .command(
462 command("test-cmd")
463 .about("Test command")
464 .arg(
465 arg("input")
466 .short('i')
467 .long("input")
468 .help("Input file")
469 .takes_value(true)
470 )
471 )
472 .handler("test-cmd", test_handler)
473 .build();
474
475 let app = Command::new("test");
476 let app_with_plugin = plugin.register_commands(app);
477
478 let plugin_cmd = app_with_plugin.find_subcommand("test-plugin");
480 assert!(plugin_cmd.is_some());
481
482 let plugin_cmd = plugin_cmd.unwrap();
483 let test_cmd = plugin_cmd.find_subcommand("test-cmd");
484 assert!(test_cmd.is_some());
485 }
486
487 #[test]
488 fn test_command_builder() {
489 let cmd = command("test")
490 .about("Test command")
491 .long_about("This is a longer description")
492 .aliases(vec!["t".to_string(), "tst".to_string()])
493 .arg(
494 arg("verbose")
495 .short('v')
496 .long("verbose")
497 .help("Enable verbose output")
498 )
499 .build();
500
501 assert_eq!(cmd.get_name(), "test");
502 assert_eq!(cmd.get_about().map(|s| s.to_string()), Some("Test command".to_string()));
503 assert_eq!(cmd.get_long_about().map(|s| s.to_string()), Some("This is a longer description".to_string()));
504
505 let aliases: Vec<&str> = cmd.get_visible_aliases().map(|a| a).collect();
507 assert!(aliases.contains(&"t"));
508 assert!(aliases.contains(&"tst"));
509
510 let verbose_arg = cmd.get_arguments().find(|a| a.get_id() == "verbose");
512 assert!(verbose_arg.is_some());
513 }
514
515 #[test]
516 fn test_arg_builder_flag() {
517 let arg = arg("verbose")
518 .short('v')
519 .long("verbose")
520 .help("Enable verbose output")
521 .build();
522
523 assert_eq!(arg.get_id().to_string(), "verbose");
524 assert_eq!(arg.get_short(), Some('v'));
525 assert_eq!(arg.get_long(), Some("verbose"));
526 assert_eq!(arg.get_help().map(|s| s.to_string()), Some("Enable verbose output".to_string()));
527 }
528
529 #[test]
530 fn test_arg_builder_with_value() {
531 let arg = arg("input")
532 .long("input")
533 .help("Input file")
534 .required(true)
535 .takes_value(true)
536 .default_value("default.txt")
537 .build();
538
539 assert_eq!(arg.get_id().to_string(), "input");
540 assert_eq!(arg.get_long(), Some("input"));
541 assert!(arg.is_required_set());
542 assert_eq!(arg.get_default_values(), &["default.txt"]);
543 }
544
545 #[test]
546 fn test_arg_builder_with_possible_values() {
547 let arg = arg("format")
548 .long("format")
549 .help("Output format")
550 .takes_value(true)
551 .possible_value("json")
552 .possible_value("yaml")
553 .possible_value("text")
554 .build();
555
556 assert_eq!(arg.get_id().to_string(), "format");
557 }
560
561 #[test]
562 fn test_command_builder_with_subcommands() {
563 let cmd = command("parent")
564 .about("Parent command")
565 .subcommand(
566 command("child1")
567 .about("First child")
568 )
569 .subcommand(
570 command("child2")
571 .about("Second child")
572 .arg(
573 arg("option")
574 .long("option")
575 .takes_value(true)
576 )
577 )
578 .build();
579
580 assert_eq!(cmd.get_name(), "parent");
581
582 let child1 = cmd.find_subcommand("child1");
583 assert!(child1.is_some());
584 assert_eq!(child1.unwrap().get_about().map(|s| s.to_string()), Some("First child".to_string()));
585
586 let child2 = cmd.find_subcommand("child2");
587 assert!(child2.is_some());
588 let child2 = child2.unwrap();
589 assert_eq!(child2.get_about().map(|s| s.to_string()), Some("Second child".to_string()));
590
591 let option_arg = child2.get_arguments().find(|a| a.get_id() == "option");
592 assert!(option_arg.is_some());
593 }
594
595 #[test]
596 fn test_command_builder_external_subcommands() {
597 let cmd = command("exec")
598 .about("Execute commands")
599 .allow_external_subcommands(true)
600 .build();
601
602 assert_eq!(cmd.get_name(), "exec");
603 assert!(cmd.is_allow_external_subcommands_set());
604 }
605
606 #[test]
607 fn test_plugin_handler_execution() {
608 use std::sync::Arc;
609 use std::sync::atomic::{AtomicBool, Ordering};
610
611 let executed = Arc::new(AtomicBool::new(false));
612 let executed_clone = executed.clone();
613
614 let test_handler = move |_matches: &ArgMatches, _config: &RuntimeConfig| -> Result<()> {
615 executed_clone.store(true, Ordering::SeqCst);
616 Ok(())
617 };
618
619 let plugin = plugin("test-plugin")
620 .command(
621 command("test-cmd")
622 .about("Test command")
623 )
624 .handler("test-cmd", test_handler)
625 .build();
626
627 let app = Command::new("test-plugin")
630 .subcommand(Command::new("test-cmd"));
631 let matches = app.clone().get_matches_from(vec!["test-plugin", "test-cmd"]);
632
633 let config = RuntimeConfig {
635 meta_config: crate::MetaConfig::default(),
636 working_dir: std::path::PathBuf::from("."),
637 meta_file_path: None,
638 experimental: false,
639 };
640
641 plugin.handle_command(&matches, &config).unwrap();
643
644 assert!(executed.load(Ordering::SeqCst));
646 }
647}