1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum PluginPermission {
11 WorkspaceRead,
12 WorkspaceWrite,
13 FilesystemRead,
14 FilesystemWrite,
15 Network,
16 AiInvoke,
17 GitRead,
18 GitWrite,
19 ExternalProcess,
20}
21
22impl PluginPermission {
23 pub fn parse(s: &str) -> Result<Self, ManifestValidationError> {
24 match s {
25 "workspace.read" => Ok(Self::WorkspaceRead),
26 "workspace.write" => Ok(Self::WorkspaceWrite),
27 "filesystem.read" => Ok(Self::FilesystemRead),
28 "filesystem.write" => Ok(Self::FilesystemWrite),
29 "network" => Ok(Self::Network),
30 "ai.invoke" => Ok(Self::AiInvoke),
31 "git.read" => Ok(Self::GitRead),
32 "git.write" => Ok(Self::GitWrite),
33 "external_process" => Ok(Self::ExternalProcess),
34 other => Err(ManifestValidationError::UnknownPermission(other.to_string())),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum PluginKind {
43 Validator,
44 Exporter,
45 Workflow,
46 Documentation,
47 Build,
48 Release,
49 Reasoner,
50 Query,
51 Ui,
52 Ai,
53}
54
55impl PluginKind {
56 pub fn as_str(&self) -> &'static str {
57 match self {
58 Self::Validator => "validator",
59 Self::Exporter => "exporter",
60 Self::Workflow => "workflow",
61 Self::Documentation => "documentation",
62 Self::Build => "build",
63 Self::Release => "release",
64 Self::Reasoner => "reasoner",
65 Self::Query => "query",
66 Self::Ui => "ui",
67 Self::Ai => "ai",
68 }
69 }
70
71 pub fn parse(s: &str) -> Result<Self, ManifestValidationError> {
72 match s {
73 "validator" => Ok(Self::Validator),
74 "exporter" => Ok(Self::Exporter),
75 "workflow" => Ok(Self::Workflow),
76 "documentation" => Ok(Self::Documentation),
77 "build" => Ok(Self::Build),
78 "release" => Ok(Self::Release),
79 "reasoner" => Ok(Self::Reasoner),
80 "query" => Ok(Self::Query),
81 "ui" => Ok(Self::Ui),
82 "ai" => Ok(Self::Ai),
83 other => Err(ManifestValidationError::UnknownKind(other.to_string())),
84 }
85 }
86}
87
88#[derive(Debug, Error)]
89pub enum ManifestValidationError {
90 #[error("unknown plugin kind: {0}")]
91 UnknownKind(String),
92 #[error("unknown plugin permission: {0}")]
93 UnknownPermission(String),
94 #[error("unsupported api_version: {0} (expected \"1\")")]
95 UnsupportedApiVersion(String),
96 #[error("missing required field: {0}")]
97 MissingField(&'static str),
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct PluginManifest {
103 pub name: String,
104 pub version: String,
105 pub kind: PluginKind,
106 pub id: Option<String>,
107 pub api_version: Option<String>,
108 pub permissions: Vec<PluginPermission>,
109 pub entry: Option<String>,
110 pub capabilities: PluginCapabilities,
111 pub config: PluginConfig,
112 pub ui: PluginUiContributions,
113}
114
115#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
116pub struct PluginConfig {
117 #[serde(default)]
118 pub require_label: bool,
119 #[serde(default)]
120 pub iri_prefix: Option<String>,
121 #[serde(default)]
122 pub shapes_dir: Option<String>,
123 #[serde(default)]
124 pub output_dir: Option<String>,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct PluginCommandContribution {
129 pub id: String,
130 pub title: String,
131 #[serde(default)]
132 pub scope: Option<String>,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct PluginViewContribution {
137 pub id: String,
138 pub title: String,
139 #[serde(default)]
141 pub kind: Option<String>,
142 #[serde(default)]
144 pub command: Option<String>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct PluginPreferencePageContribution {
149 pub id: String,
150 pub title: String,
151 #[serde(default)]
152 pub category: Option<String>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct PluginContextActionContribution {
157 pub id: String,
158 pub title: String,
159 #[serde(default)]
161 pub scope: Option<String>,
162 #[serde(default)]
164 pub applies_to: Vec<String>,
165 pub command: String,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170pub struct PluginInspectorCard {
171 pub id: String,
172 pub title: String,
173 #[serde(default)]
174 pub applies_to: Vec<String>,
175 #[serde(default)]
176 pub command: Option<String>,
177}
178
179#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
180pub struct PluginUiContributions {
181 #[serde(default)]
182 pub commands: Vec<PluginCommandContribution>,
183 #[serde(default)]
184 pub views: Vec<PluginViewContribution>,
185 #[serde(default, rename = "preferences_pages")]
186 pub preferences_pages: Vec<PluginPreferencePageContribution>,
187 #[serde(default, rename = "context_actions")]
188 pub context_actions: Vec<PluginContextActionContribution>,
189 #[serde(default, rename = "inspector_cards")]
190 pub inspector_cards: Vec<PluginInspectorCard>,
191}
192
193#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
194pub struct PluginCapabilities {
195 #[serde(default)]
196 pub build: bool,
197 #[serde(default)]
198 pub validate: bool,
199 #[serde(default)]
200 pub release: bool,
201 #[serde(default)]
202 pub diagnostics: bool,
203 #[serde(default)]
204 pub export: bool,
205}
206
207impl PluginCapabilities {
208 pub fn supports_validation(&self) -> bool {
209 self.validate || self.diagnostics
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct DiscoveredPlugin {
215 pub manifest: PluginManifest,
216 pub manifest_path: PathBuf,
217}
218
219impl DiscoveredPlugin {
220 pub fn plugin_id(&self) -> &str {
221 self.manifest.id.as_deref().unwrap_or(&self.manifest.name)
222 }
223}
224
225#[derive(Debug, Deserialize)]
226struct ManifestFile {
227 plugin: PluginSection,
228 #[serde(default)]
229 capabilities: PluginCapabilities,
230 #[serde(default)]
231 config: PluginConfig,
232 #[serde(default)]
233 ui: PluginUiContributions,
234}
235
236#[derive(Debug, Deserialize)]
237struct PluginSection {
238 name: String,
239 version: String,
240 kind: String,
241 #[serde(default)]
242 id: Option<String>,
243 #[serde(default)]
244 api_version: Option<String>,
245 #[serde(default)]
246 permissions: Vec<String>,
247 #[serde(default)]
248 entry: Option<String>,
249}
250
251pub fn parse_manifest(text: &str) -> Result<PluginManifest, ManifestValidationError> {
253 let file: ManifestFile = toml::from_str(text)
254 .map_err(|_| ManifestValidationError::MissingField("valid TOML structure"))?;
255 if file.plugin.name.is_empty() {
256 return Err(ManifestValidationError::MissingField("plugin.name"));
257 }
258 if file.plugin.version.is_empty() {
259 return Err(ManifestValidationError::MissingField("plugin.version"));
260 }
261 if let Some(api) = &file.plugin.api_version {
262 if api != "1" {
263 return Err(ManifestValidationError::UnsupportedApiVersion(api.clone()));
264 }
265 }
266 let kind = PluginKind::parse(&file.plugin.kind)?;
267 let mut permissions = Vec::new();
268 for p in &file.plugin.permissions {
269 permissions.push(PluginPermission::parse(p)?);
270 }
271 if permissions.is_empty() {
274 permissions.push(PluginPermission::WorkspaceRead);
275 if file.plugin.entry.is_some() {
276 permissions.push(PluginPermission::ExternalProcess);
277 }
278 }
279 Ok(PluginManifest {
280 name: file.plugin.name,
281 version: file.plugin.version,
282 kind,
283 id: file.plugin.id,
284 api_version: file.plugin.api_version,
285 permissions,
286 entry: file.plugin.entry,
287 capabilities: file.capabilities,
288 config: file.config,
289 ui: file.ui,
290 })
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn parses_example_manifest() {
299 let text = r#"
300[plugin]
301name = "example-workflow"
302version = "0.1.0"
303kind = "workflow"
304
305[capabilities]
306build = true
307validate = true
308"#;
309 let manifest = parse_manifest(text).expect("parse");
310 assert_eq!(manifest.name, "example-workflow");
311 assert_eq!(manifest.kind, PluginKind::Workflow);
312 assert!(manifest.capabilities.build);
313 assert!(manifest.capabilities.validate);
314 }
315
316 #[test]
317 fn rejects_unknown_kind() {
318 let text = r#"
319[plugin]
320name = "bad"
321version = "0.1.0"
322kind = "not-a-kind"
323"#;
324 assert!(parse_manifest(text).is_err());
325 }
326
327 #[test]
328 fn parses_ui_contributions() {
329 let text = r#"
330[plugin]
331name = "naming"
332version = "0.1.0"
333kind = "validator"
334id = "ontocode.naming-validator"
335permissions = ["workspace.read"]
336
337[[ui.commands]]
338id = "naming.check"
339title = "Check naming conventions"
340
341[[ui.views]]
342id = "naming.view"
343title = "Naming view"
344
345[[ui.preferences_pages]]
346id = "naming.prefs"
347title = "Naming"
348
349[[ui.context_actions]]
350id = "naming.ctx"
351title = "Check naming for class"
352scope = "entity"
353applies_to = ["class"]
354command = "naming.check"
355
356[[ui.inspector_cards]]
357id = "naming-summary"
358title = "Naming"
359applies_to = ["class"]
360"#;
361 let manifest = parse_manifest(text).expect("parse");
362 assert_eq!(manifest.ui.commands.len(), 1);
363 assert_eq!(manifest.ui.views.len(), 1);
364 assert_eq!(manifest.ui.preferences_pages.len(), 1);
365 assert_eq!(manifest.ui.context_actions.len(), 1);
366 assert_eq!(manifest.ui.inspector_cards.len(), 1);
367 assert_eq!(manifest.permissions.len(), 1);
368 }
369}