mod template;
pub use template::{generate_plugin, PluginOptions, TemplateError, TemplateResult};
use thiserror::Error;
pub use vanguard_plugin::{PluginMetadata, ValidationResult, VanguardPlugin};
#[derive(Error, Debug)]
pub enum PluginError {
#[error("Validation failed: {0}")]
ValidationFailed(String),
#[error("Initialization failed: {0}")]
InitializationFailed(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}
pub type PluginResult<T> = Result<T, PluginError>;
pub trait PluginMetadataBuilder {
fn name(self, name: impl Into<String>) -> Self;
fn version(self, version: impl Into<String>) -> Self;
fn description(self, description: impl Into<String>) -> Self;
fn author(self, author: impl Into<String>) -> Self;
fn min_vanguard_version(self, version: impl Into<String>) -> Self;
fn max_vanguard_version(self, version: impl Into<String>) -> Self;
fn build(self) -> PluginMetadata;
}
#[derive(Debug, Default)]
pub struct MetadataBuilder {
name: Option<String>,
version: Option<String>,
description: Option<String>,
author: Option<String>,
min_vanguard_version: Option<String>,
max_vanguard_version: Option<String>,
}
impl PluginMetadataBuilder for MetadataBuilder {
fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
fn author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
fn min_vanguard_version(mut self, version: impl Into<String>) -> Self {
self.min_vanguard_version = Some(version.into());
self
}
fn max_vanguard_version(mut self, version: impl Into<String>) -> Self {
self.max_vanguard_version = Some(version.into());
self
}
fn build(self) -> PluginMetadata {
PluginMetadata {
name: self.name.unwrap_or_else(|| "unnamed".to_string()),
version: self.version.unwrap_or_else(|| "0.1.0".to_string()),
description: self
.description
.unwrap_or_else(|| "No description".to_string()),
author: self.author.unwrap_or_else(|| "Unknown".to_string()),
min_vanguard_version: self.min_vanguard_version,
max_vanguard_version: self.max_vanguard_version,
dependencies: vec![],
}
}
}
pub fn metadata() -> MetadataBuilder {
MetadataBuilder::default()
}
#[macro_export]
macro_rules! plugin {
($plugin:ident, $config:ident) => {
#[async_trait::async_trait]
impl $crate::VanguardPlugin for $plugin {
fn metadata(&self) -> &$crate::PluginMetadata {
&self.metadata
}
async fn validate(&self) -> $crate::ValidationResult {
if let Some(config) = &self.config {
if let Err(e) = config.validate() {
return $crate::ValidationResult::Failed(e.to_string());
}
}
$crate::ValidationResult::Passed
}
async fn initialize(&self) -> Result<(), String> {
Ok(())
}
async fn cleanup(&self) -> Result<(), String> {
Ok(())
}
fn config_schema(&self) -> Option<serde_json::Value> {
Some($config::schema())
}
}
#[cfg(not(test))]
#[no_mangle]
pub extern "C" fn create_plugin() -> *mut $plugin {
Box::into_raw(Box::new($plugin::new()))
}
#[cfg(not(test))]
#[no_mangle]
pub unsafe extern "C" fn destroy_plugin(plugin: *mut $plugin) {
if !plugin.is_null() {
let _ = Box::from_raw(plugin);
}
}
#[cfg(test)]
#[allow(private_interfaces)]
#[no_mangle]
pub extern "C" fn create_plugin() -> *mut $plugin {
Box::into_raw(Box::new($plugin::new()))
}
#[cfg(test)]
#[allow(private_interfaces)]
#[no_mangle]
pub unsafe extern "C" fn destroy_plugin(plugin: *mut $plugin) {
if !plugin.is_null() {
let _ = Box::from_raw(plugin);
}
}
};
}
#[macro_export]
macro_rules! plugin_config {
($config:ident, $schema:expr) => {
impl $config {
pub fn schema() -> serde_json::Value {
$schema
}
pub fn validate(&self) -> Result<(), String> {
Ok(())
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct TestConfig {
value: String,
}
plugin_config!(
TestConfig,
serde_json::json!({
"type": "object",
"properties": {
"value": {
"type": "string"
}
}
})
);
#[derive(Debug)]
struct TestPlugin {
metadata: PluginMetadata,
config: Option<TestConfig>,
}
impl TestPlugin {
fn new() -> Self {
Self {
metadata: metadata()
.name("test")
.version("1.0.0")
.description("Test plugin")
.author("Test Author")
.build(),
config: None,
}
}
}
plugin!(TestPlugin, TestConfig);
#[tokio::test]
async fn test_plugin_metadata() {
let plugin = TestPlugin::new();
assert_eq!(plugin.metadata().name, "test");
assert_eq!(plugin.metadata().version, "1.0.0");
}
#[tokio::test]
async fn test_plugin_validation() {
let plugin = TestPlugin::new();
assert!(matches!(plugin.validate().await, ValidationResult::Passed));
}
}
pub mod command;
pub use command::{Command, CommandContext, CommandHandler, CommandResult, VanguardCommand};
#[doc(hidden)]
pub fn _update_plugin_macro() {
}
#[macro_export]
macro_rules! command_handler {
($plugin:ident) => {
#[async_trait::async_trait]
impl $crate::command::CommandHandler for $plugin {
fn get_commands(&self) -> Vec<$crate::command::Command> {
Vec::new()
}
async fn handle_command(
&self,
_command: &$crate::command::VanguardCommand,
_ctx: &$crate::command::CommandContext,
) -> $crate::command::CommandResult {
$crate::command::CommandResult::NotHandled
}
}
};
($plugin:ident, $commands:expr) => {
#[async_trait::async_trait]
impl $crate::command::CommandHandler for $plugin {
fn get_commands(&self) -> Vec<$crate::command::Command> {
$commands
}
async fn handle_command(
&self,
_command: &$crate::command::VanguardCommand,
_ctx: &$crate::command::CommandContext,
) -> $crate::command::CommandResult {
$crate::command::CommandResult::NotHandled
}
}
};
($plugin:ident, $commands:expr, $handler:expr) => {
#[async_trait::async_trait]
impl $crate::command::CommandHandler for $plugin {
fn get_commands(&self) -> Vec<$crate::command::Command> {
$commands
}
async fn handle_command(
&self,
command: &$crate::command::VanguardCommand,
ctx: &$crate::command::CommandContext,
) -> $crate::command::CommandResult {
($handler)(self, command, ctx).await
}
}
};
}