Skip to main content

ontocore_plugin/
manifest.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use thiserror::Error;
4
5/// Supported plugin kinds per PLUGIN_SPEC.
6#[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/// Parsed plugin manifest from workspace TOML.
65#[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
172/// Parse and validate a plugin manifest from TOML text.
173pub 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}