use std::{fs, path::Path};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TemplateError {
#[error("Invalid plugin name: {0}")]
InvalidName(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
pub type TemplateResult<T> = Result<T, TemplateError>;
#[derive(Debug, Clone)]
pub struct PluginOptions {
pub name: String,
pub description: String,
pub author: String,
pub version: String,
pub min_vanguard_version: Option<String>,
pub template: Option<String>,
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c.is_alphanumeric() {
if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
} else {
capitalize_next = true;
}
}
result
}
fn is_inside_vanguard_repo() -> bool {
let current_dir = std::env::current_dir().ok();
if let Some(dir) = current_dir {
let mut path = dir.clone();
loop {
if path.join("crates").join("vanguard-plugin-sdk").exists() {
return true;
}
if let Some(parent) = path.parent() {
path = parent.to_path_buf();
} else {
break;
}
}
}
false
}
fn generate_cargo_toml(
plugin_name: &str,
description: &str,
_author: &str,
version: &str,
) -> String {
let inside_vanguard_repo = is_inside_vanguard_repo();
let sdk_dependency = if inside_vanguard_repo {
"vanguard-plugin-sdk = { path = \"../../crates/vanguard-plugin-sdk\" }".to_string()
} else {
"vanguard-plugin-sdk = \"0.1.4\"".to_string()
};
format!(
r#"[package]
name = "{plugin_name}"
version = "{version}"
edition = "2021"
description = "{description}"
license = "MIT"
[lib]
crate-type = ["cdylib"]
[dependencies]
{sdk_dependency}
async-trait = "0.1"
serde = {{ version = "1.0", features = ["derive"] }}
serde_json = "1.0"
tokio = {{ version = "1.0", features = ["full"] }}
[dev-dependencies]
tokio-test = "0.4"
# Make this plugin independent from the parent workspace
[workspace]
"#
)
}
fn generate_readme(plugin_name: &str, description: &str, author: &str) -> String {
let inside_vanguard_repo = is_inside_vanguard_repo();
let installation_note = if inside_vanguard_repo {
"This plugin is configured to work within the Vanguard repository. If you want to use it outside the repository, you'll need to update the dependency in Cargo.toml."
} else {
"This plugin uses the vanguard-plugin-sdk from crates.io and can be built anywhere without requiring the Vanguard source code."
};
format!(
r#"# {plugin_name}
{description}
## Author
{author}
## Installation
1. Build the plugin:
```bash
cargo build --release
```
2. Install the plugin in Vanguard:
```bash
# On macOS:
vanguard plugin install ./target/release/lib{plugin_name}.dylib
# On Linux:
vanguard plugin install ./target/release/lib{plugin_name}.so
# On Windows:
vanguard plugin install ./target/release/{plugin_name}.dll
```
## Notes
{installation_note}
"#
)
}
fn generate_lib_rs(
plugin_name: &str,
struct_name: &str,
description: &str,
author: &str,
version: &str,
min_vanguard_version: Option<&str>,
) -> String {
let min_version = min_vanguard_version.unwrap_or("0.1.0");
format!(
r#"use serde::{{Deserialize, Serialize}};
use vanguard_plugin_sdk::{{metadata, plugin, plugin_config, PluginMetadata, PluginMetadataBuilder}};
/// Configuration for the {plugin_name} plugin
#[derive(Debug, Serialize, Deserialize)]
pub struct {struct_name}Config {{
/// Example configuration field
pub value: String,
}}
plugin_config!({struct_name}Config, serde_json::json!({{
"type": "object",
"required": ["value"],
"properties": {{
"value": {{
"type": "string",
"description": "Example configuration value"
}}
}}
}}));
/// A plugin that {description}
#[derive(Debug)]
pub struct {struct_name}Plugin {{
metadata: PluginMetadata,
config: Option<{struct_name}Config>,
}}
impl {struct_name}Plugin {{
/// Create a new plugin instance
pub fn new() -> Self {{
Self {{
metadata: metadata()
.name("{plugin_name}")
.version("{version}")
.description("{description}")
.author("{author}")
.min_vanguard_version("{min_version}")
.build(),
config: None,
}}
}}
}}
plugin!({struct_name}Plugin, {struct_name}Config);
#[cfg(test)]
mod tests {{
use super::*;
use vanguard_plugin_sdk::{{ValidationResult, VanguardPlugin}};
#[tokio::test]
async fn test_plugin_metadata() {{
let plugin = {struct_name}Plugin::new();
assert_eq!(plugin.metadata().name, "{plugin_name}");
assert_eq!(plugin.metadata().version, "{version}");
}}
#[tokio::test]
async fn test_plugin_validation() {{
let plugin = {struct_name}Plugin::new();
assert!(matches!(plugin.validate().await, ValidationResult::Passed));
}}
}}
"#
)
}
fn generate_lib_rs_with_commands(
plugin_name: &str,
struct_name: &str,
description: &str,
author: &str,
version: &str,
min_vanguard_version: Option<&str>,
) -> String {
let min_version = min_vanguard_version.unwrap_or("0.1.0");
format!(
r#"use serde::{{Deserialize, Serialize}};
use vanguard_plugin_sdk::{{
command::{{Command, CommandContext, CommandResult, VanguardCommand}},
command_handler, metadata, plugin, plugin_config, PluginMetadata, PluginMetadataBuilder
}};
/// Configuration for the {plugin_name} plugin
#[derive(Debug, Serialize, Deserialize)]
pub struct {struct_name}Config {{
/// Example configuration field
pub value: String,
}}
plugin_config!({struct_name}Config, serde_json::json!({{
"type": "object",
"required": ["value"],
"properties": {{
"value": {{
"type": "string",
"description": "Example configuration value"
}}
}}
}}));
/// A plugin that {description}
#[derive(Debug)]
pub struct {struct_name}Plugin {{
metadata: PluginMetadata,
config: Option<{struct_name}Config>,
}}
impl {struct_name}Plugin {{
/// Create a new plugin instance
pub fn new() -> Self {{
Self {{
metadata: metadata()
.name("{plugin_name}")
.version("{version}")
.description("{description}")
.author("{author}")
.min_vanguard_version("{min_version}")
.build(),
config: None,
}}
}}
/// Example command handler function
fn handle_hello_command(&self, args: &[String]) -> String {{
let name = args.get(0).cloned().unwrap_or_else(|| "World".to_string());
format!("Hello, {{}}! This message is from the {plugin_name} plugin!", name)
}}
}}
plugin!({struct_name}Plugin, {struct_name}Config);
// Implement command handling for the plugin
command_handler!({struct_name}Plugin,
vec![
Command {{
name: "hello".to_string(),
description: "Says hello from the plugin".to_string(),
usage: "{plugin_name} hello [name]".to_string(),
aliases: vec!["hi".to_string(), "greet".to_string()],
}}
],
|plugin: &{struct_name}Plugin, command: &VanguardCommand, _ctx: &CommandContext| {{
// Handle the command synchronously
let result = match command.name.as_str() {{
"hello" | "hi" | "greet" => {{
let message = plugin.handle_hello_command(&command.args);
println!("{{}}", message);
CommandResult::Success
}},
_ => CommandResult::NotHandled,
}};
// Return a future that resolves to the result
Box::pin(async move {{ result }})
}}
);
#[cfg(test)]
mod tests {{
use super::*;
use vanguard_plugin_sdk::{{ValidationResult, VanguardPlugin}};
#[tokio::test]
async fn test_plugin_metadata() {{
let plugin = {struct_name}Plugin::new();
assert_eq!(plugin.metadata().name, "{plugin_name}");
assert_eq!(plugin.metadata().version, "{version}");
}}
#[tokio::test]
async fn test_plugin_validation() {{
let plugin = {struct_name}Plugin::new();
assert!(matches!(plugin.validate().await, ValidationResult::Passed));
}}
#[tokio::test]
async fn test_command_handling() {{
let plugin = {struct_name}Plugin::new();
let cmd = VanguardCommand {{
name: "hello".to_string(),
args: vec!["Tester".to_string()],
original: "hello Tester".to_string(),
}};
let ctx = CommandContext::default();
let result = plugin.handle_command(&cmd, &ctx).await;
assert!(matches!(result, CommandResult::Success));
}}
}}
"#
)
}
pub fn generate_plugin(path: impl AsRef<Path>, options: PluginOptions) -> TemplateResult<()> {
let path = path.as_ref();
let name = options.name.to_lowercase();
if name.is_empty()
|| !name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
return Err(TemplateError::InvalidName(name));
}
fs::create_dir_all(path)?;
let struct_name = to_pascal_case(&options.name);
let src_dir = path.join("src");
fs::create_dir_all(&src_dir)?;
let readme_path = path.join("README.md");
let readme_content = generate_readme(&options.name, &options.description, &options.author);
fs::write(readme_path, readme_content)?;
let cargo_toml_path = path.join("Cargo.toml");
let cargo_toml_content = generate_cargo_toml(
&options.name,
&options.description,
&options.author,
&options.version,
);
fs::write(cargo_toml_path, cargo_toml_content)?;
let lib_rs_path = src_dir.join("lib.rs");
let lib_rs_content = match options.template.as_deref() {
Some("commands") => generate_lib_rs_with_commands(
&options.name,
&struct_name,
&options.description,
&options.author,
&options.version,
options.min_vanguard_version.as_deref(),
),
_ => generate_lib_rs(
&options.name,
&struct_name,
&options.description,
&options.author,
&options.version,
options.min_vanguard_version.as_deref(),
),
};
fs::write(lib_rs_path, lib_rs_content)?;
Ok(())
}