1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use thiserror::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum PluginKind {
9 Validator,
10 Exporter,
11 Workflow,
12 Documentation,
13 Build,
14 Release,
15 Reasoner,
16 Query,
17 Ui,
18 Ai,
19}
20
21impl PluginKind {
22 pub fn as_str(&self) -> &'static str {
23 match self {
24 Self::Validator => "validator",
25 Self::Exporter => "exporter",
26 Self::Workflow => "workflow",
27 Self::Documentation => "documentation",
28 Self::Build => "build",
29 Self::Release => "release",
30 Self::Reasoner => "reasoner",
31 Self::Query => "query",
32 Self::Ui => "ui",
33 Self::Ai => "ai",
34 }
35 }
36
37 pub fn parse(s: &str) -> Result<Self, ManifestValidationError> {
38 match s {
39 "validator" => Ok(Self::Validator),
40 "exporter" => Ok(Self::Exporter),
41 "workflow" => Ok(Self::Workflow),
42 "documentation" => Ok(Self::Documentation),
43 "build" => Ok(Self::Build),
44 "release" => Ok(Self::Release),
45 "reasoner" => Ok(Self::Reasoner),
46 "query" => Ok(Self::Query),
47 "ui" => Ok(Self::Ui),
48 "ai" => Ok(Self::Ai),
49 other => Err(ManifestValidationError::UnknownKind(other.to_string())),
50 }
51 }
52}
53
54#[derive(Debug, Error)]
55pub enum ManifestValidationError {
56 #[error("unknown plugin kind: {0}")]
57 UnknownKind(String),
58 #[error("unsupported api_version: {0} (expected \"1\")")]
59 UnsupportedApiVersion(String),
60 #[error("missing required field: {0}")]
61 MissingField(&'static str),
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct PluginManifest {
67 pub name: String,
68 pub version: String,
69 pub kind: PluginKind,
70 pub id: Option<String>,
71 pub api_version: Option<String>,
72 pub entry: Option<String>,
73 pub capabilities: PluginCapabilities,
74 pub config: PluginConfig,
75 pub ui: PluginUiContributions,
76}
77
78#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
79pub struct PluginConfig {
80 #[serde(default)]
81 pub require_label: bool,
82 #[serde(default)]
83 pub iri_prefix: Option<String>,
84 #[serde(default)]
85 pub shapes_dir: Option<String>,
86 #[serde(default)]
87 pub output_dir: Option<String>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct PluginCommandContribution {
92 pub id: String,
93 pub title: String,
94 #[serde(default)]
95 pub scope: Option<String>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct PluginInspectorCard {
100 pub id: String,
101 pub title: String,
102 #[serde(default)]
103 pub applies_to: Vec<String>,
104 #[serde(default)]
105 pub command: Option<String>,
106}
107
108#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
109pub struct PluginUiContributions {
110 #[serde(default)]
111 pub commands: Vec<PluginCommandContribution>,
112 #[serde(default, rename = "inspector_cards")]
113 pub inspector_cards: Vec<PluginInspectorCard>,
114}
115
116#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
117pub struct PluginCapabilities {
118 #[serde(default)]
119 pub build: bool,
120 #[serde(default)]
121 pub validate: bool,
122 #[serde(default)]
123 pub release: bool,
124 #[serde(default)]
125 pub diagnostics: bool,
126 #[serde(default)]
127 pub export: bool,
128}
129
130impl PluginCapabilities {
131 pub fn supports_validation(&self) -> bool {
132 self.validate || self.diagnostics
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct DiscoveredPlugin {
138 pub manifest: PluginManifest,
139 pub manifest_path: PathBuf,
140}
141
142impl DiscoveredPlugin {
143 pub fn plugin_id(&self) -> &str {
144 self.manifest.id.as_deref().unwrap_or(&self.manifest.name)
145 }
146}
147
148#[derive(Debug, Deserialize)]
149struct ManifestFile {
150 plugin: PluginSection,
151 #[serde(default)]
152 capabilities: PluginCapabilities,
153 #[serde(default)]
154 config: PluginConfig,
155 #[serde(default)]
156 ui: PluginUiContributions,
157}
158
159#[derive(Debug, Deserialize)]
160struct PluginSection {
161 name: String,
162 version: String,
163 kind: String,
164 #[serde(default)]
165 id: Option<String>,
166 #[serde(default)]
167 api_version: Option<String>,
168 #[serde(default)]
169 entry: Option<String>,
170}
171
172pub fn parse_manifest(text: &str) -> Result<PluginManifest, ManifestValidationError> {
174 let file: ManifestFile = toml::from_str(text)
175 .map_err(|_| ManifestValidationError::MissingField("valid TOML structure"))?;
176 if file.plugin.name.is_empty() {
177 return Err(ManifestValidationError::MissingField("plugin.name"));
178 }
179 if file.plugin.version.is_empty() {
180 return Err(ManifestValidationError::MissingField("plugin.version"));
181 }
182 if let Some(api) = &file.plugin.api_version {
183 if api != "1" {
184 return Err(ManifestValidationError::UnsupportedApiVersion(api.clone()));
185 }
186 }
187 let kind = PluginKind::parse(&file.plugin.kind)?;
188 Ok(PluginManifest {
189 name: file.plugin.name,
190 version: file.plugin.version,
191 kind,
192 id: file.plugin.id,
193 api_version: file.plugin.api_version,
194 entry: file.plugin.entry,
195 capabilities: file.capabilities,
196 config: file.config,
197 ui: file.ui,
198 })
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn parses_example_manifest() {
207 let text = r#"
208[plugin]
209name = "example-workflow"
210version = "0.1.0"
211kind = "workflow"
212
213[capabilities]
214build = true
215validate = true
216"#;
217 let manifest = parse_manifest(text).expect("parse");
218 assert_eq!(manifest.name, "example-workflow");
219 assert_eq!(manifest.kind, PluginKind::Workflow);
220 assert!(manifest.capabilities.build);
221 assert!(manifest.capabilities.validate);
222 }
223
224 #[test]
225 fn rejects_unknown_kind() {
226 let text = r#"
227[plugin]
228name = "bad"
229version = "0.1.0"
230kind = "not-a-kind"
231"#;
232 assert!(parse_manifest(text).is_err());
233 }
234
235 #[test]
236 fn parses_ui_contributions() {
237 let text = r#"
238[plugin]
239name = "naming"
240version = "0.1.0"
241kind = "validator"
242id = "ontocode.naming-validator"
243
244[[ui.commands]]
245id = "naming.check"
246title = "Check naming conventions"
247
248[[ui.inspector_cards]]
249id = "naming-summary"
250title = "Naming"
251applies_to = ["class"]
252"#;
253 let manifest = parse_manifest(text).expect("parse");
254 assert_eq!(manifest.ui.commands.len(), 1);
255 assert_eq!(manifest.ui.inspector_cards.len(), 1);
256 }
257}