orbis_plugin_api/
manifest.rs

1//! Plugin manifest definition.
2
3use serde::{Deserialize, Serialize};
4use semver::Version;
5
6/// Plugin manifest describing the plugin's metadata, routes, and pages.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct PluginManifest {
9    /// Plugin name (unique identifier).
10    pub name: String,
11
12    /// Plugin version (semver).
13    pub version: String,
14
15    /// Human-readable description.
16    #[serde(default)]
17    pub description: String,
18
19    /// Plugin author.
20    #[serde(default)]
21    pub author: Option<String>,
22
23    /// Plugin homepage URL.
24    #[serde(default)]
25    pub homepage: Option<String>,
26
27    /// Plugin license.
28    #[serde(default)]
29    pub license: Option<String>,
30
31    /// Minimum Orbis version required.
32    #[serde(default)]
33    pub min_orbis_version: Option<String>,
34
35    /// Plugin dependencies.
36    #[serde(default)]
37    pub dependencies: Vec<PluginDependency>,
38
39    /// Required permissions.
40    #[serde(default)]
41    pub permissions: Vec<PluginPermission>,
42
43    /// API routes defined by the plugin.
44    #[serde(default)]
45    pub routes: Vec<PluginRoute>,
46
47    /// UI pages defined by the plugin.
48    #[serde(default)]
49    pub pages: Vec<crate::ui::PageDefinition>,
50
51    /// Entry point for WASM plugins (relative path in unpacked/packed).
52    #[serde(default)]
53    pub wasm_entry: Option<String>,
54
55    /// Additional custom configuration.
56    #[serde(default)]
57    pub config: serde_json::Value,
58}
59
60impl PluginManifest {
61    /// Validate the manifest.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if the manifest is invalid.
66    pub fn validate(&self) -> crate::Result<()> {
67        // Validate name
68        if self.name.is_empty() {
69            return Err(crate::Error::manifest("Plugin name is required"));
70        }
71
72        if !self.name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
73            return Err(crate::Error::manifest(
74                "Plugin name must contain only alphanumeric characters, hyphens, and underscores",
75            ));
76        }
77
78        // Validate version
79        if self.version.is_empty() {
80            return Err(crate::Error::manifest("Plugin version is required"));
81        }
82
83        Version::parse(&self.version).map_err(|e| {
84            crate::Error::manifest(format!("Invalid plugin version '{}': {}", self.version, e))
85        })?;
86
87        // Validate routes
88        for route in &self.routes {
89            route.validate()?;
90        }
91
92        // Validate pages
93        for page in &self.pages {
94            page.validate()?;
95        }
96
97        Ok(())
98    }
99
100    /// Get the parsed semver version.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if the version is invalid.
105    pub fn parsed_version(&self) -> crate::Result<Version> {
106        Version::parse(&self.version)
107            .map_err(|e| crate::Error::manifest(format!("Invalid version: {}", e)))
108    }
109}
110
111/// Plugin dependency.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct PluginDependency {
114    /// Dependency name.
115    pub name: String,
116
117    /// Version requirement (semver).
118    pub version: String,
119
120    /// Whether the dependency is optional.
121    #[serde(default)]
122    pub optional: bool,
123}
124
125/// Plugin permission.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum PluginPermission {
129    /// Read from database.
130    DatabaseRead,
131
132    /// Write to database.
133    DatabaseWrite,
134
135    /// Read files.
136    FileRead,
137
138    /// Write files.
139    FileWrite,
140
141    /// Make network requests.
142    Network,
143
144    /// Access system information.
145    System,
146
147    /// Execute shell commands (dangerous).
148    Shell,
149
150    /// Access environment variables.
151    Environment,
152
153    /// Custom permission.
154    Custom(String),
155}
156
157/// API route definition.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct PluginRoute {
160    /// HTTP method.
161    pub method: String,
162
163    /// Route path (relative to plugin prefix).
164    pub path: String,
165
166    /// Handler function name.
167    pub handler: String,
168
169    /// Route description.
170    #[serde(default)]
171    pub description: Option<String>,
172
173    /// Whether authentication is required.
174    #[serde(default = "default_true")]
175    pub requires_auth: bool,
176
177    /// Required permissions.
178    #[serde(default)]
179    pub permissions: Vec<String>,
180
181    /// Rate limit (requests per minute).
182    #[serde(default)]
183    pub rate_limit: Option<u32>,
184}
185
186fn default_true() -> bool {
187    true
188}
189
190impl PluginRoute {
191    /// Validate the route.
192    ///
193    /// # Errors
194    ///
195    /// Returns an error if the route is invalid.
196    pub fn validate(&self) -> crate::Result<()> {
197        // Validate method
198        let valid_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
199        if !valid_methods.contains(&self.method.to_uppercase().as_str()) {
200            return Err(crate::Error::manifest(format!(
201                "Invalid HTTP method: {}",
202                self.method
203            )));
204        }
205
206        // Validate path
207        if self.path.is_empty() {
208            return Err(crate::Error::manifest("Route path is required"));
209        }
210
211        if !self.path.starts_with('/') {
212            return Err(crate::Error::manifest("Route path must start with '/'"));
213        }
214
215        // Validate handler
216        if self.handler.is_empty() {
217            return Err(crate::Error::manifest("Route handler is required"));
218        }
219
220        Ok(())
221    }
222
223    /// Get the full route path with plugin prefix.
224    #[must_use]
225    pub fn full_path(&self, plugin_name: &str) -> String {
226        format!("/api/plugins/{}{}", plugin_name, self.path)
227    }
228}