vanguard_plugin_sdk/
template.rs

1use std::{fs, path::Path};
2use thiserror::Error;
3
4/// Plugin template generation errors
5#[derive(Error, Debug)]
6pub enum TemplateError {
7    #[error("Invalid plugin name: {0}")]
8    InvalidName(String),
9
10    #[error("I/O error: {0}")]
11    Io(#[from] std::io::Error),
12}
13
14/// Result type for template operations
15pub type TemplateResult<T> = Result<T, TemplateError>;
16
17/// Options for plugin generation
18#[derive(Debug, Clone)]
19pub struct PluginOptions {
20    /// Plugin name (crate name)
21    pub name: String,
22    /// Plugin description
23    pub description: String,
24    /// Plugin author
25    pub author: String,
26    /// Plugin version
27    pub version: String,
28    /// Minimum required Vanguard version
29    pub min_vanguard_version: Option<String>,
30    /// Template to use (basic or commands)
31    pub template: Option<String>,
32}
33
34/// Convert a string to PascalCase
35///
36/// - "my-plugin" -> "MyPlugin"
37/// - "my_plugin" -> "MyPlugin"
38/// - "my plugin" -> "MyPlugin"
39fn to_pascal_case(s: &str) -> String {
40    let mut result = String::new();
41    let mut capitalize_next = true;
42
43    for c in s.chars() {
44        if c.is_alphanumeric() {
45            if capitalize_next {
46                result.push(c.to_ascii_uppercase());
47                capitalize_next = false;
48            } else {
49                result.push(c);
50            }
51        } else {
52            capitalize_next = true;
53        }
54    }
55
56    result
57}
58
59/// Detect if we're running inside the Vanguard repo
60fn is_inside_vanguard_repo() -> bool {
61    // Try to find the Vanguard repo root by checking for specific directories and files
62    let current_dir = std::env::current_dir().ok();
63    if let Some(dir) = current_dir {
64        // Look for markers of the Vanguard repo
65        let mut path = dir.clone();
66        loop {
67            // Check if this directory contains crates/vanguard-plugin-sdk
68            if path.join("crates").join("vanguard-plugin-sdk").exists() {
69                return true;
70            }
71
72            // Try parent directory
73            if let Some(parent) = path.parent() {
74                path = parent.to_path_buf();
75            } else {
76                break;
77            }
78        }
79    }
80    false
81}
82
83/// Generate the Cargo.toml template
84fn generate_cargo_toml(
85    plugin_name: &str,
86    description: &str,
87    _author: &str,
88    version: &str,
89) -> String {
90    let inside_vanguard_repo = is_inside_vanguard_repo();
91
92    // Determine the SDK dependency line based on whether we're inside the repo
93    let sdk_dependency = if inside_vanguard_repo {
94        // Use local path dependency if inside the Vanguard repo
95        "vanguard-plugin-sdk = { path = \"../../crates/vanguard-plugin-sdk\" }".to_string()
96    } else {
97        // Use published version from crates.io
98        "vanguard-plugin-sdk = \"0.1.3\"".to_string()
99    };
100
101    format!(
102        r#"[package]
103name = "{plugin_name}"
104version = "{version}"
105edition = "2021"
106description = "{description}"
107license = "MIT"
108
109[lib]
110crate-type = ["cdylib"]
111
112[dependencies]
113{sdk_dependency}
114async-trait = "0.1"
115serde = {{ version = "1.0", features = ["derive"] }}
116serde_json = "1.0"
117tokio = {{ version = "1.0", features = ["full"] }}
118
119[dev-dependencies]
120tokio-test = "0.4"
121
122# Make this plugin independent from the parent workspace
123[workspace]
124"#
125    )
126}
127
128/// Generate a basic plugin README
129fn generate_readme(plugin_name: &str, description: &str, author: &str) -> String {
130    let inside_vanguard_repo = is_inside_vanguard_repo();
131
132    let installation_note = if inside_vanguard_repo {
133        "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."
134    } else {
135        "This plugin uses the vanguard-plugin-sdk from crates.io and can be built anywhere without requiring the Vanguard source code."
136    };
137
138    format!(
139        r#"# {plugin_name}
140
141{description}
142
143## Author
144
145{author}
146
147## Installation
148
1491. Build the plugin:
150   ```bash
151   cargo build --release
152   ```
153
1542. Install the plugin in Vanguard:
155   ```bash
156   # On macOS:
157   vanguard plugin install ./target/release/lib{plugin_name}.dylib
158   
159   # On Linux:
160   vanguard plugin install ./target/release/lib{plugin_name}.so
161   
162   # On Windows:
163   vanguard plugin install ./target/release/{plugin_name}.dll
164   ```
165
166## Notes
167
168{installation_note}
169"#
170    )
171}
172
173/// Generate lib.rs content
174fn generate_lib_rs(
175    plugin_name: &str,
176    struct_name: &str,
177    description: &str,
178    author: &str,
179    version: &str,
180    min_vanguard_version: Option<&str>,
181) -> String {
182    let min_version = min_vanguard_version.unwrap_or("0.1.0");
183
184    format!(
185        r#"use serde::{{Deserialize, Serialize}};
186use vanguard_plugin_sdk::{{metadata, plugin, plugin_config, PluginMetadata, PluginMetadataBuilder}};
187
188/// Configuration for the {plugin_name} plugin
189#[derive(Debug, Serialize, Deserialize)]
190pub struct {struct_name}Config {{
191    /// Example configuration field
192    pub value: String,
193}}
194
195plugin_config!({struct_name}Config, serde_json::json!({{
196    "type": "object",
197    "required": ["value"],
198    "properties": {{
199        "value": {{
200            "type": "string",
201            "description": "Example configuration value"
202        }}
203    }}
204}}));
205
206/// A plugin that {description}
207#[derive(Debug)]
208pub struct {struct_name}Plugin {{
209    metadata: PluginMetadata,
210    config: Option<{struct_name}Config>,
211}}
212
213impl {struct_name}Plugin {{
214    /// Create a new plugin instance
215    pub fn new() -> Self {{
216        Self {{
217            metadata: metadata()
218                .name("{plugin_name}")
219                .version("{version}")
220                .description("{description}")
221                .author("{author}")
222                .min_vanguard_version("{min_version}")
223                .build(),
224            config: None,
225        }}
226    }}
227}}
228
229plugin!({struct_name}Plugin, {struct_name}Config);
230
231#[cfg(test)]
232mod tests {{
233    use super::*;
234    use vanguard_plugin_sdk::{{ValidationResult, VanguardPlugin}};
235
236    #[tokio::test]
237    async fn test_plugin_metadata() {{
238        let plugin = {struct_name}Plugin::new();
239        assert_eq!(plugin.metadata().name, "{plugin_name}");
240        assert_eq!(plugin.metadata().version, "{version}");
241    }}
242
243    #[tokio::test]
244    async fn test_plugin_validation() {{
245        let plugin = {struct_name}Plugin::new();
246        assert!(matches!(plugin.validate().await, ValidationResult::Passed));
247    }}
248}}
249"#
250    )
251}
252
253/// Generate lib.rs content with command support
254fn generate_lib_rs_with_commands(
255    plugin_name: &str,
256    struct_name: &str,
257    description: &str,
258    author: &str,
259    version: &str,
260    min_vanguard_version: Option<&str>,
261) -> String {
262    let min_version = min_vanguard_version.unwrap_or("0.1.0");
263
264    format!(
265        r#"use serde::{{Deserialize, Serialize}};
266use vanguard_plugin_sdk::{{
267    command::{{Command, CommandContext, CommandResult, VanguardCommand}},
268    command_handler, metadata, plugin, plugin_config, PluginMetadata, PluginMetadataBuilder
269}};
270
271/// Configuration for the {plugin_name} plugin
272#[derive(Debug, Serialize, Deserialize)]
273pub struct {struct_name}Config {{
274    /// Example configuration field
275    pub value: String,
276}}
277
278plugin_config!({struct_name}Config, serde_json::json!({{
279    "type": "object",
280    "required": ["value"],
281    "properties": {{
282        "value": {{
283            "type": "string",
284            "description": "Example configuration value"
285        }}
286    }}
287}}));
288
289/// A plugin that {description}
290#[derive(Debug)]
291pub struct {struct_name}Plugin {{
292    metadata: PluginMetadata,
293    config: Option<{struct_name}Config>,
294}}
295
296impl {struct_name}Plugin {{
297    /// Create a new plugin instance
298    pub fn new() -> Self {{
299        Self {{
300            metadata: metadata()
301                .name("{plugin_name}")
302                .version("{version}")
303                .description("{description}")
304                .author("{author}")
305                .min_vanguard_version("{min_version}")
306                .build(),
307            config: None,
308        }}
309    }}
310
311    /// Example command handler function
312    async fn handle_hello_command(&self, args: &[String]) -> CommandResult {{
313        let name = args.get(0).cloned().unwrap_or_else(|| "World".to_string());
314        println!("Hello, {{}}! This message is from the {plugin_name} plugin!", name);
315        CommandResult::Success
316    }}
317}}
318
319plugin!({struct_name}Plugin, {struct_name}Config);
320
321// Implement command handling for the plugin
322command_handler!({struct_name}Plugin, 
323    vec![
324        Command {{
325            name: "hello".to_string(),
326            description: "Says hello from the plugin".to_string(),
327            usage: "{plugin_name} hello [name]".to_string(),
328            aliases: vec!["hi".to_string(), "greet".to_string()],
329        }}
330    ],
331    |plugin: &{struct_name}Plugin, command: &VanguardCommand, _ctx: &CommandContext| async move {{
332        match command.name.as_str() {{
333            "hello" | "hi" | "greet" => plugin.handle_hello_command(&command.args).await,
334            _ => CommandResult::NotHandled,
335        }}
336    }}
337);
338
339#[cfg(test)]
340mod tests {{
341    use super::*;
342    use vanguard_plugin_sdk::{{ValidationResult, VanguardPlugin}};
343
344    #[tokio::test]
345    async fn test_plugin_metadata() {{
346        let plugin = {struct_name}Plugin::new();
347        assert_eq!(plugin.metadata().name, "{plugin_name}");
348        assert_eq!(plugin.metadata().version, "{version}");
349    }}
350
351    #[tokio::test]
352    async fn test_plugin_validation() {{
353        let plugin = {struct_name}Plugin::new();
354        assert!(matches!(plugin.validate().await, ValidationResult::Passed));
355    }}
356
357    #[tokio::test]
358    async fn test_command_handling() {{
359        let plugin = {struct_name}Plugin::new();
360        let cmd = VanguardCommand {{
361            name: "hello".to_string(),
362            args: vec!["Tester".to_string()],
363            original: "hello Tester".to_string(),
364        }};
365        let ctx = CommandContext::default();
366        let result = plugin.handle_command(&cmd, &ctx).await;
367        assert!(matches!(result, CommandResult::Success));
368    }}
369}}
370"#
371    )
372}
373
374/// Generate a new plugin from a template
375pub fn generate_plugin(path: impl AsRef<Path>, options: PluginOptions) -> TemplateResult<()> {
376    let path = path.as_ref();
377
378    // Validate plugin name
379    let name = options.name.to_lowercase();
380    if name.is_empty()
381        || !name
382            .chars()
383            .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
384    {
385        return Err(TemplateError::InvalidName(name));
386    }
387
388    // Create plugin directory
389    fs::create_dir_all(path)?;
390
391    // Generate plugin files
392    let struct_name = to_pascal_case(&options.name);
393
394    // Create the src directory
395    let src_dir = path.join("src");
396    fs::create_dir_all(&src_dir)?;
397
398    // Create the README.md
399    let readme_path = path.join("README.md");
400    let readme_content = generate_readme(&options.name, &options.description, &options.author);
401    fs::write(readme_path, readme_content)?;
402
403    // Create Cargo.toml
404    let cargo_toml_path = path.join("Cargo.toml");
405    let cargo_toml_content = generate_cargo_toml(
406        &options.name,
407        &options.description,
408        &options.author,
409        &options.version,
410    );
411    fs::write(cargo_toml_path, cargo_toml_content)?;
412
413    // Create lib.rs with the appropriate template
414    let lib_rs_path = src_dir.join("lib.rs");
415    let lib_rs_content = match options.template.as_deref() {
416        Some("commands") => generate_lib_rs_with_commands(
417            &options.name,
418            &struct_name,
419            &options.description,
420            &options.author,
421            &options.version,
422            options.min_vanguard_version.as_deref(),
423        ),
424        _ => generate_lib_rs(
425            &options.name,
426            &struct_name,
427            &options.description,
428            &options.author,
429            &options.version,
430            options.min_vanguard_version.as_deref(),
431        ),
432    };
433    fs::write(lib_rs_path, lib_rs_content)?;
434
435    Ok(())
436}