use serde::{Deserialize, Serialize};
use semver::Version;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
pub name: String,
pub version: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub homepage: Option<String>,
#[serde(default)]
pub license: Option<String>,
#[serde(default)]
pub min_orbis_version: Option<String>,
#[serde(default)]
pub dependencies: Vec<PluginDependency>,
#[serde(default)]
pub permissions: Vec<PluginPermission>,
#[serde(default)]
pub routes: Vec<PluginRoute>,
#[serde(default)]
pub pages: Vec<crate::ui::PageDefinition>,
#[serde(default)]
pub wasm_entry: Option<String>,
#[serde(default)]
pub config: serde_json::Value,
}
impl PluginManifest {
pub fn validate(&self) -> crate::Result<()> {
if self.name.is_empty() {
return Err(crate::Error::manifest("Plugin name is required"));
}
if !self.name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
return Err(crate::Error::manifest(
"Plugin name must contain only alphanumeric characters, hyphens, and underscores",
));
}
if self.version.is_empty() {
return Err(crate::Error::manifest("Plugin version is required"));
}
Version::parse(&self.version).map_err(|e| {
crate::Error::manifest(format!("Invalid plugin version '{}': {}", self.version, e))
})?;
for route in &self.routes {
route.validate()?;
}
for page in &self.pages {
page.validate()?;
}
Ok(())
}
pub fn parsed_version(&self) -> crate::Result<Version> {
Version::parse(&self.version)
.map_err(|e| crate::Error::manifest(format!("Invalid version: {}", e)))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginDependency {
pub name: String,
pub version: String,
#[serde(default)]
pub optional: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PluginPermission {
DatabaseRead,
DatabaseWrite,
FileRead,
FileWrite,
Network,
System,
Shell,
Environment,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginRoute {
pub method: String,
pub path: String,
pub handler: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default = "default_true")]
pub requires_auth: bool,
#[serde(default)]
pub permissions: Vec<String>,
#[serde(default)]
pub rate_limit: Option<u32>,
}
fn default_true() -> bool {
true
}
impl PluginRoute {
pub fn validate(&self) -> crate::Result<()> {
let valid_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
if !valid_methods.contains(&self.method.to_uppercase().as_str()) {
return Err(crate::Error::manifest(format!(
"Invalid HTTP method: {}",
self.method
)));
}
if self.path.is_empty() {
return Err(crate::Error::manifest("Route path is required"));
}
if !self.path.starts_with('/') {
return Err(crate::Error::manifest("Route path must start with '/'"));
}
if self.handler.is_empty() {
return Err(crate::Error::manifest("Route handler is required"));
}
Ok(())
}
#[must_use]
pub fn full_path(&self, plugin_name: &str) -> String {
format!("/api/plugins/{}{}", plugin_name, self.path)
}
}