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.4\"".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    fn handle_hello_command(&self, args: &[String]) -> String {{
313        let name = args.get(0).cloned().unwrap_or_else(|| "World".to_string());
314        format!("Hello, {{}}! This message is from the {plugin_name} plugin!", name)
315    }}
316}}
317
318plugin!({struct_name}Plugin, {struct_name}Config);
319
320// Implement command handling for the plugin
321command_handler!({struct_name}Plugin, 
322    vec![
323        Command {{
324            name: "hello".to_string(),
325            description: "Says hello from the plugin".to_string(),
326            usage: "{plugin_name} hello [name]".to_string(),
327            aliases: vec!["hi".to_string(), "greet".to_string()],
328        }}
329    ],
330    |plugin: &{struct_name}Plugin, command: &VanguardCommand, _ctx: &CommandContext| {{
331        // Handle the command synchronously
332        let result = match command.name.as_str() {{
333            "hello" | "hi" | "greet" => {{
334                let message = plugin.handle_hello_command(&command.args);
335                println!("{{}}", message);
336                CommandResult::Success
337            }},
338            _ => CommandResult::NotHandled,
339        }};
340        
341        // Return a future that resolves to the result
342        Box::pin(async move {{ result }})
343    }}
344);
345
346#[cfg(test)]
347mod tests {{
348    use super::*;
349    use vanguard_plugin_sdk::{{ValidationResult, VanguardPlugin}};
350
351    #[tokio::test]
352    async fn test_plugin_metadata() {{
353        let plugin = {struct_name}Plugin::new();
354        assert_eq!(plugin.metadata().name, "{plugin_name}");
355        assert_eq!(plugin.metadata().version, "{version}");
356    }}
357
358    #[tokio::test]
359    async fn test_plugin_validation() {{
360        let plugin = {struct_name}Plugin::new();
361        assert!(matches!(plugin.validate().await, ValidationResult::Passed));
362    }}
363
364    #[tokio::test]
365    async fn test_command_handling() {{
366        let plugin = {struct_name}Plugin::new();
367        let cmd = VanguardCommand {{
368            name: "hello".to_string(),
369            args: vec!["Tester".to_string()],
370            original: "hello Tester".to_string(),
371        }};
372        let ctx = CommandContext::default();
373        let result = plugin.handle_command(&cmd, &ctx).await;
374        assert!(matches!(result, CommandResult::Success));
375    }}
376}}
377"#
378    )
379}
380
381/// Generate a new plugin from a template
382pub fn generate_plugin(path: impl AsRef<Path>, options: PluginOptions) -> TemplateResult<()> {
383    let path = path.as_ref();
384
385    // Validate plugin name
386    let name = options.name.to_lowercase();
387    if name.is_empty()
388        || !name
389            .chars()
390            .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
391    {
392        return Err(TemplateError::InvalidName(name));
393    }
394
395    // Create plugin directory
396    fs::create_dir_all(path)?;
397
398    // Generate plugin files
399    let struct_name = to_pascal_case(&options.name);
400
401    // Create the src directory
402    let src_dir = path.join("src");
403    fs::create_dir_all(&src_dir)?;
404
405    // Create the README.md
406    let readme_path = path.join("README.md");
407    let readme_content = generate_readme(&options.name, &options.description, &options.author);
408    fs::write(readme_path, readme_content)?;
409
410    // Create Cargo.toml
411    let cargo_toml_path = path.join("Cargo.toml");
412    let cargo_toml_content = generate_cargo_toml(
413        &options.name,
414        &options.description,
415        &options.author,
416        &options.version,
417    );
418    fs::write(cargo_toml_path, cargo_toml_content)?;
419
420    // Create lib.rs with the appropriate template
421    let lib_rs_path = src_dir.join("lib.rs");
422    let lib_rs_content = match options.template.as_deref() {
423        Some("commands") => generate_lib_rs_with_commands(
424            &options.name,
425            &struct_name,
426            &options.description,
427            &options.author,
428            &options.version,
429            options.min_vanguard_version.as_deref(),
430        ),
431        _ => generate_lib_rs(
432            &options.name,
433            &struct_name,
434            &options.description,
435            &options.author,
436            &options.version,
437            options.min_vanguard_version.as_deref(),
438        ),
439    };
440    fs::write(lib_rs_path, lib_rs_content)?;
441
442    Ok(())
443}