use anyhow::Result;
use clap::Command;
use serde::{Serialize, Deserialize};
use std::fmt;
use crate::MetaPlugin;
pub trait BasePlugin: MetaPlugin {
fn metadata(&self) -> PluginMetadata {
PluginMetadata {
name: self.name().to_string(),
version: self.version().unwrap_or("0.1.0").to_string(),
description: self.description().unwrap_or("").to_string(),
author: self.author().unwrap_or("").to_string(),
experimental: self.is_experimental(),
}
}
fn version(&self) -> Option<&str> {
None
}
fn description(&self) -> Option<&str> {
None
}
fn author(&self) -> Option<&str> {
None
}
fn show_help(&self, format: HelpFormat) -> Result<()> {
let app = self.build_help_command();
let formatter = format.formatter();
formatter.format_help(&app)
}
fn build_help_command(&self) -> Command {
let name: &'static str = Box::leak(format!("meta {}", self.name()).into_boxed_str());
let app = Command::new(name);
self.register_commands(app)
}
fn show_ai_help(&self) -> Result<()> {
self.show_help(HelpFormat::Json)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginMetadata {
pub name: String,
pub version: String,
pub description: String,
pub author: String,
pub experimental: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HelpFormat {
Terminal,
Json,
Yaml,
Markdown,
}
impl HelpFormat {
pub fn formatter(&self) -> Box<dyn HelpFormatter> {
match self {
HelpFormat::Terminal => Box::new(TerminalHelpFormatter),
HelpFormat::Json => Box::new(JsonHelpFormatter),
HelpFormat::Yaml => Box::new(YamlHelpFormatter),
HelpFormat::Markdown => Box::new(MarkdownHelpFormatter),
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"terminal" | "term" => Some(HelpFormat::Terminal),
"json" => Some(HelpFormat::Json),
"yaml" | "yml" => Some(HelpFormat::Yaml),
"markdown" | "md" => Some(HelpFormat::Markdown),
_ => None,
}
}
}
impl fmt::Display for HelpFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HelpFormat::Terminal => write!(f, "terminal"),
HelpFormat::Json => write!(f, "json"),
HelpFormat::Yaml => write!(f, "yaml"),
HelpFormat::Markdown => write!(f, "markdown"),
}
}
}
pub trait HelpFormatter {
fn format_help(&self, app: &Command) -> Result<()>;
}
pub struct TerminalHelpFormatter;
impl HelpFormatter for TerminalHelpFormatter {
fn format_help(&self, app: &Command) -> Result<()> {
let mut app = app.clone();
app.print_help()?;
println!();
Ok(())
}
}
pub struct JsonHelpFormatter;
impl HelpFormatter for JsonHelpFormatter {
fn format_help(&self, app: &Command) -> Result<()> {
let help_data = extract_command_info(app);
let json = serde_json::to_string_pretty(&help_data)?;
println!("{}", json);
Ok(())
}
}
pub struct YamlHelpFormatter;
impl HelpFormatter for YamlHelpFormatter {
fn format_help(&self, app: &Command) -> Result<()> {
let help_data = extract_command_info(app);
let yaml = serde_yaml::to_string(&help_data)?;
println!("{}", yaml);
Ok(())
}
}
pub struct MarkdownHelpFormatter;
impl HelpFormatter for MarkdownHelpFormatter {
fn format_help(&self, app: &Command) -> Result<()> {
let mut output = String::new();
output.push_str(&format!("# {}\n\n", app.get_name()));
if let Some(about) = app.get_about() {
output.push_str(&format!("{}\n\n", about));
}
output.push_str("## Usage\n\n```\n");
output.push_str(&format!("{} [OPTIONS]", app.get_name()));
if app.get_subcommands().count() > 0 {
output.push_str(" <COMMAND>");
}
output.push_str("\n```\n\n");
let args: Vec<_> = app.get_arguments().collect();
if !args.is_empty() {
output.push_str("## Options\n\n");
for arg in args {
if let Some(help) = arg.get_help() {
let short = arg.get_short().map(|s| format!("-{}", s)).unwrap_or_default();
let long = arg.get_long().map(|l| format!("--{}", l)).unwrap_or_default();
let flags = match (&short[..], &long[..]) {
("", l) => l.to_string(),
(s, "") => s.to_string(),
(s, l) => format!("{}, {}", s, l),
};
output.push_str(&format!("- `{}`: {}\n", flags, help));
}
}
output.push('\n');
}
let subcommands: Vec<_> = app.get_subcommands().collect();
if !subcommands.is_empty() {
output.push_str("## Commands\n\n");
for subcmd in subcommands {
output.push_str(&format!("### {}\n\n", subcmd.get_name()));
if let Some(about) = subcmd.get_about() {
output.push_str(&format!("{}\n\n", about));
}
}
}
println!("{}", output);
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CommandInfo {
pub name: String,
pub description: Option<String>,
pub version: Option<String>,
pub subcommands: Vec<CommandInfo>,
pub arguments: Vec<ArgumentInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ArgumentInfo {
pub name: String,
pub short: Option<char>,
pub long: Option<String>,
pub help: Option<String>,
pub required: bool,
pub takes_value: bool,
}
fn extract_command_info(app: &Command) -> CommandInfo {
CommandInfo {
name: app.get_name().to_string(),
description: app.get_about().map(|s| s.to_string()),
version: app.get_version().map(|s| s.to_string()),
subcommands: app.get_subcommands()
.map(|cmd| extract_command_info(cmd))
.collect(),
arguments: app.get_arguments()
.map(|arg| ArgumentInfo {
name: arg.get_id().to_string(),
short: arg.get_short(),
long: arg.get_long().map(|s| s.to_string()),
help: arg.get_help().map(|s| s.to_string()),
required: arg.is_required_set(),
takes_value: arg.get_num_args().map(|n| n.takes_values()).unwrap_or(false),
})
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_help_format_from_str() {
assert_eq!(HelpFormat::from_str("terminal"), Some(HelpFormat::Terminal));
assert_eq!(HelpFormat::from_str("term"), Some(HelpFormat::Terminal));
assert_eq!(HelpFormat::from_str("json"), Some(HelpFormat::Json));
assert_eq!(HelpFormat::from_str("yaml"), Some(HelpFormat::Yaml));
assert_eq!(HelpFormat::from_str("yml"), Some(HelpFormat::Yaml));
assert_eq!(HelpFormat::from_str("markdown"), Some(HelpFormat::Markdown));
assert_eq!(HelpFormat::from_str("md"), Some(HelpFormat::Markdown));
assert_eq!(HelpFormat::from_str("unknown"), None);
assert_eq!(HelpFormat::from_str("JSON"), Some(HelpFormat::Json));
assert_eq!(HelpFormat::from_str("Terminal"), Some(HelpFormat::Terminal));
}
#[test]
fn test_help_format_display() {
assert_eq!(format!("{}", HelpFormat::Terminal), "terminal");
assert_eq!(format!("{}", HelpFormat::Json), "json");
assert_eq!(format!("{}", HelpFormat::Yaml), "yaml");
assert_eq!(format!("{}", HelpFormat::Markdown), "markdown");
}
#[test]
fn test_extract_command_info() {
let app = Command::new("test-app")
.version("1.0.0")
.about("Test application")
.arg(
clap::Arg::new("verbose")
.short('v')
.long("verbose")
.help("Enable verbose output")
)
.arg(
clap::Arg::new("input")
.long("input")
.help("Input file")
.required(true)
.value_name("FILE")
)
.subcommand(
Command::new("sub")
.about("Subcommand")
.arg(
clap::Arg::new("flag")
.short('f')
.help("A flag")
)
);
let info = extract_command_info(&app);
assert_eq!(info.name, "test-app");
assert_eq!(info.description, Some("Test application".to_string()));
assert_eq!(info.version, Some("1.0.0".to_string()));
assert_eq!(info.subcommands.len(), 1);
assert_eq!(info.subcommands[0].name, "sub");
let verbose_arg = info.arguments.iter().find(|a| a.name == "verbose");
assert!(verbose_arg.is_some());
let verbose = verbose_arg.unwrap();
assert_eq!(verbose.short, Some('v'));
assert_eq!(verbose.long, Some("verbose".to_string()));
assert_eq!(verbose.help, Some("Enable verbose output".to_string()));
let input_arg = info.arguments.iter().find(|a| a.name == "input");
assert!(input_arg.is_some());
let input = input_arg.unwrap();
assert_eq!(input.long, Some("input".to_string()));
assert!(input.required);
}
#[test]
fn test_plugin_metadata() {
#[derive(Debug)]
struct TestPlugin;
impl MetaPlugin for TestPlugin {
fn name(&self) -> &str {
"test"
}
fn register_commands(&self, app: Command) -> Command {
app
}
fn handle_command(&self, _matches: &clap::ArgMatches, _config: &crate::RuntimeConfig) -> Result<()> {
Ok(())
}
fn is_experimental(&self) -> bool {
true
}
}
impl BasePlugin for TestPlugin {
fn version(&self) -> Option<&str> {
Some("1.2.3")
}
fn description(&self) -> Option<&str> {
Some("Test plugin")
}
fn author(&self) -> Option<&str> {
Some("Test Author")
}
}
let plugin = TestPlugin;
let metadata = plugin.metadata();
assert_eq!(metadata.name, "test");
assert_eq!(metadata.version, "1.2.3");
assert_eq!(metadata.description, "Test plugin");
assert_eq!(metadata.author, "Test Author");
assert!(metadata.experimental);
}
}