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}
31
32/// Convert a string to PascalCase
33///
34/// - "my-plugin" -> "MyPlugin"
35/// - "my_plugin" -> "MyPlugin"
36/// - "my plugin" -> "MyPlugin"
37fn to_pascal_case(s: &str) -> String {
38    let mut result = String::new();
39    let mut capitalize_next = true;
40
41    for c in s.chars() {
42        if c.is_alphanumeric() {
43            if capitalize_next {
44                result.push(c.to_ascii_uppercase());
45                capitalize_next = false;
46            } else {
47                result.push(c);
48            }
49        } else {
50            capitalize_next = true;
51        }
52    }
53
54    result
55}
56
57/// Detect if we're running inside the Vanguard repo
58fn is_inside_vanguard_repo() -> bool {
59    // Try to find the Vanguard repo root by checking for specific directories and files
60    let current_dir = std::env::current_dir().ok();
61    if let Some(dir) = current_dir {
62        // Look for markers of the Vanguard repo
63        let mut path = dir.clone();
64        loop {
65            // Check if this directory contains crates/vanguard-plugin-sdk
66            if path.join("crates").join("vanguard-plugin-sdk").exists() {
67                return true;
68            }
69
70            // Try parent directory
71            if let Some(parent) = path.parent() {
72                path = parent.to_path_buf();
73            } else {
74                break;
75            }
76        }
77    }
78    false
79}
80
81/// Generate the Cargo.toml template
82fn generate_cargo_toml(
83    plugin_name: &str,
84    description: &str,
85    _author: &str,
86    version: &str,
87) -> String {
88    let inside_vanguard_repo = is_inside_vanguard_repo();
89    
90    // Determine the SDK dependency line based on whether we're inside the repo
91    let sdk_dependency = if inside_vanguard_repo {
92        // Use local path dependency if inside the Vanguard repo
93        "vanguard-plugin-sdk = { path = \"../../crates/vanguard-plugin-sdk\" }".to_string()
94    } else {
95        // Check if we have a local SDK package in ~/.vanguard/sdk
96        if let Some(home_dir) = std::env::var_os("HOME").map(std::path::PathBuf::from) {
97            let local_sdk_path = home_dir.join(".vanguard").join("sdk");
98            
99            if local_sdk_path.exists() {
100                // Use the local SDK package
101                format!("vanguard-plugin-sdk = {{ path = \"{}\" }}", 
102                    local_sdk_path.to_string_lossy())
103            } else {
104                // If no local SDK, fall back to git dependency
105                "vanguard-plugin-sdk = { git = \"https://github.com/find-how/pioneer-vanguard\", branch = \"main\" }".to_string()
106            }
107        } else {
108            // Fallback if HOME is not set
109            "vanguard-plugin-sdk = { git = \"https://github.com/find-how/pioneer-vanguard\", branch = \"main\" }".to_string()
110        }
111    };
112    
113    format!(
114        r#"[package]
115name = "{plugin_name}"
116version = "{version}"
117edition = "2021"
118description = "{description}"
119license = "MIT"
120
121[lib]
122crate-type = ["cdylib"]
123
124[dependencies]
125{sdk_dependency}
126async-trait = "0.1"
127serde = {{ version = "1.0", features = ["derive"] }}
128serde_json = "1.0"
129tokio = {{ version = "1.0", features = ["full"] }}
130
131[dev-dependencies]
132tokio-test = "0.4"
133
134# Make this plugin independent from the parent workspace
135[workspace]
136"#
137    )
138}
139
140/// Generate a basic plugin README
141fn generate_readme(plugin_name: &str, description: &str, author: &str) -> String {
142    let inside_vanguard_repo = is_inside_vanguard_repo();
143    
144    // Check for local SDK
145    let using_local_sdk = if inside_vanguard_repo {
146        false
147    } else {
148        if let Some(home_dir) = std::env::var_os("HOME").map(std::path::PathBuf::from) {
149            let local_sdk_path = home_dir.join(".vanguard").join("sdk");
150            local_sdk_path.exists()
151        } else {
152            false
153        }
154    };
155    
156    let installation_note = if inside_vanguard_repo {
157        "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."
158    } else if using_local_sdk {
159        "This plugin is configured to use a local SDK installation. It doesn't require the Vanguard source code repository."
160    } else {
161        "This plugin is configured to work independently from the Vanguard repository. It uses the SDK from the main GitHub repository."
162    };
163    
164    format!(
165        r#"# {plugin_name}
166
167{description}
168
169## Author
170
171{author}
172
173## Installation
174
1751. Build the plugin:
176   ```bash
177   cargo build --release
178   ```
179
1802. Install the plugin in Vanguard:
181   ```bash
182   # On macOS:
183   vanguard plugin install ./target/release/lib{plugin_name}.dylib
184   
185   # On Linux:
186   vanguard plugin install ./target/release/lib{plugin_name}.so
187   
188   # On Windows:
189   vanguard plugin install ./target/release/{plugin_name}.dll
190   ```
191
192## Notes
193
194{installation_note}
195"#
196    )
197}
198
199/// Generate lib.rs content
200fn generate_lib_rs(
201    plugin_name: &str,
202    struct_name: &str,
203    description: &str,
204    author: &str,
205    version: &str,
206    min_vanguard_version: Option<&str>,
207) -> String {
208    let min_version = min_vanguard_version.unwrap_or("0.1.0");
209
210    format!(
211        r#"use serde::{{Deserialize, Serialize}};
212use vanguard_plugin_sdk::{{metadata, plugin, plugin_config, PluginMetadata, PluginMetadataBuilder}};
213
214/// Configuration for the {plugin_name} plugin
215#[derive(Debug, Serialize, Deserialize)]
216pub struct {struct_name}Config {{
217    /// Example configuration field
218    pub value: String,
219}}
220
221plugin_config!({struct_name}Config, serde_json::json!({{
222    "type": "object",
223    "required": ["value"],
224    "properties": {{
225        "value": {{
226            "type": "string",
227            "description": "Example configuration value"
228        }}
229    }}
230}}));
231
232/// A plugin that {description}
233#[derive(Debug)]
234pub struct {struct_name}Plugin {{
235    metadata: PluginMetadata,
236    config: Option<{struct_name}Config>,
237}}
238
239impl {struct_name}Plugin {{
240    /// Create a new plugin instance
241    pub fn new() -> Self {{
242        Self {{
243            metadata: metadata()
244                .name("{plugin_name}")
245                .version("{version}")
246                .description("{description}")
247                .author("{author}")
248                .min_vanguard_version("{min_version}")
249                .build(),
250            config: None,
251        }}
252    }}
253}}
254
255plugin!({struct_name}Plugin, {struct_name}Config);
256
257#[cfg(test)]
258mod tests {{
259    use super::*;
260    use vanguard_plugin_sdk::{{ValidationResult, VanguardPlugin}};
261
262    #[tokio::test]
263    async fn test_plugin_metadata() {{
264        let plugin = {struct_name}Plugin::new();
265        assert_eq!(plugin.metadata().name, "{plugin_name}");
266        assert_eq!(plugin.metadata().version, "{version}");
267    }}
268
269    #[tokio::test]
270    async fn test_plugin_validation() {{
271        let plugin = {struct_name}Plugin::new();
272        assert!(matches!(plugin.validate().await, ValidationResult::Passed));
273    }}
274}}
275"#
276    )
277}
278
279/// Generate a new plugin from a template
280pub fn generate_plugin(path: impl AsRef<Path>, options: PluginOptions) -> TemplateResult<()> {
281    let path = path.as_ref();
282
283    // Validate plugin name
284    let name = options.name.to_lowercase();
285    if name.is_empty()
286        || !name
287            .chars()
288            .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
289    {
290        return Err(TemplateError::InvalidName(name));
291    }
292
293    // Create plugin directory
294    fs::create_dir_all(path)?;
295
296    // Generate plugin files
297    let struct_name = to_pascal_case(&options.name);
298
299    // Create the src directory
300    let src_dir = path.join("src");
301    fs::create_dir_all(&src_dir)?;
302
303    // Create the README.md
304    let readme_path = path.join("README.md");
305    let readme_content = generate_readme(&options.name, &options.description, &options.author);
306    fs::write(readme_path, readme_content)?;
307
308    // Create Cargo.toml
309    let cargo_toml_path = path.join("Cargo.toml");
310    let cargo_toml_content = generate_cargo_toml(
311        &options.name,
312        &options.description,
313        &options.author,
314        &options.version,
315    );
316    fs::write(cargo_toml_path, cargo_toml_content)?;
317
318    // Create lib.rs
319    let lib_rs_path = src_dir.join("lib.rs");
320    let lib_rs_content = generate_lib_rs(
321        &options.name,
322        &struct_name,
323        &options.description,
324        &options.author,
325        &options.version,
326        options.min_vanguard_version.as_deref(),
327    );
328    fs::write(lib_rs_path, lib_rs_content)?;
329
330    Ok(())
331}