orbis_plugin_api/
manifest.rs1use serde::{Deserialize, Serialize};
4use semver::Version;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct PluginManifest {
9 pub name: String,
11
12 pub version: String,
14
15 #[serde(default)]
17 pub description: String,
18
19 #[serde(default)]
21 pub author: Option<String>,
22
23 #[serde(default)]
25 pub homepage: Option<String>,
26
27 #[serde(default)]
29 pub license: Option<String>,
30
31 #[serde(default)]
33 pub min_orbis_version: Option<String>,
34
35 #[serde(default)]
37 pub dependencies: Vec<PluginDependency>,
38
39 #[serde(default)]
41 pub permissions: Vec<PluginPermission>,
42
43 #[serde(default)]
45 pub routes: Vec<PluginRoute>,
46
47 #[serde(default)]
49 pub pages: Vec<crate::ui::PageDefinition>,
50
51 #[serde(default)]
53 pub wasm_entry: Option<String>,
54
55 #[serde(default)]
57 pub config: serde_json::Value,
58}
59
60impl PluginManifest {
61 pub fn validate(&self) -> crate::Result<()> {
67 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 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 for route in &self.routes {
89 route.validate()?;
90 }
91
92 for page in &self.pages {
94 page.validate()?;
95 }
96
97 Ok(())
98 }
99
100 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#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct PluginDependency {
114 pub name: String,
116
117 pub version: String,
119
120 #[serde(default)]
122 pub optional: bool,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum PluginPermission {
129 DatabaseRead,
131
132 DatabaseWrite,
134
135 FileRead,
137
138 FileWrite,
140
141 Network,
143
144 System,
146
147 Shell,
149
150 Environment,
152
153 Custom(String),
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct PluginRoute {
160 pub method: String,
162
163 pub path: String,
165
166 pub handler: String,
168
169 #[serde(default)]
171 pub description: Option<String>,
172
173 #[serde(default = "default_true")]
175 pub requires_auth: bool,
176
177 #[serde(default)]
179 pub permissions: Vec<String>,
180
181 #[serde(default)]
183 pub rate_limit: Option<u32>,
184}
185
186fn default_true() -> bool {
187 true
188}
189
190impl PluginRoute {
191 pub fn validate(&self) -> crate::Result<()> {
197 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 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 if self.handler.is_empty() {
217 return Err(crate::Error::manifest("Route handler is required"));
218 }
219
220 Ok(())
221 }
222
223 #[must_use]
225 pub fn full_path(&self, plugin_name: &str) -> String {
226 format!("/api/plugins/{}{}", plugin_name, self.path)
227 }
228}