vanguard_plugin_sdk/
template.rs1use std::{fs, path::Path};
2use thiserror::Error;
3
4#[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
14pub type TemplateResult<T> = Result<T, TemplateError>;
16
17#[derive(Debug, Clone)]
19pub struct PluginOptions {
20 pub name: String,
22 pub description: String,
24 pub author: String,
26 pub version: String,
28 pub min_vanguard_version: Option<String>,
30}
31
32fn 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
57fn is_inside_vanguard_repo() -> bool {
59 let current_dir = std::env::current_dir().ok();
61 if let Some(dir) = current_dir {
62 let mut path = dir.clone();
64 loop {
65 if path.join("crates").join("vanguard-plugin-sdk").exists() {
67 return true;
68 }
69
70 if let Some(parent) = path.parent() {
72 path = parent.to_path_buf();
73 } else {
74 break;
75 }
76 }
77 }
78 false
79}
80
81fn 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 let sdk_dependency = if inside_vanguard_repo {
92 "vanguard-plugin-sdk = { path = \"../../crates/vanguard-plugin-sdk\" }".to_string()
94 } else {
95 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 format!("vanguard-plugin-sdk = {{ path = \"{}\" }}",
102 local_sdk_path.to_string_lossy())
103 } else {
104 "vanguard-plugin-sdk = { git = \"https://github.com/find-how/pioneer-vanguard\", branch = \"main\" }".to_string()
106 }
107 } else {
108 "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
140fn generate_readme(plugin_name: &str, description: &str, author: &str) -> String {
142 let inside_vanguard_repo = is_inside_vanguard_repo();
143
144 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
199fn 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
279pub fn generate_plugin(path: impl AsRef<Path>, options: PluginOptions) -> TemplateResult<()> {
281 let path = path.as_ref();
282
283 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 fs::create_dir_all(path)?;
295
296 let struct_name = to_pascal_case(&options.name);
298
299 let src_dir = path.join("src");
301 fs::create_dir_all(&src_dir)?;
302
303 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 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 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}