1use crate::discovery::{discover_plugins, PluginDiscoveryError};
2use crate::manifest::{DiscoveredPlugin, PluginKind, PluginManifest};
3use crate::protocol::{plugin_diagnostic, PluginOutput};
4use crate::subprocess::{run_plugin_subprocess, SubprocessError, SubprocessRequest};
5use crate::traits::{ExporterPlugin, ValidatorPlugin, WorkflowPlugin, WorkflowRequest};
6use ontocore_catalog::OntologyCatalog;
7use ontocore_core::{Diagnostic, DiagnosticSeverity};
8use ontocore_docs::ExportOptions;
9use serde::Serialize;
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use thiserror::Error;
13
14#[derive(Debug, Error)]
15pub enum PluginHostError {
16 #[error(transparent)]
17 Discovery(#[from] PluginDiscoveryError),
18 #[error(transparent)]
19 Subprocess(#[from] SubprocessError),
20 #[error("plugin not found: {0}")]
21 NotFound(String),
22 #[error("plugin {0} does not support action {1}")]
23 UnsupportedAction(String, String),
24 #[error("export failed: {0}")]
25 Export(String),
26}
27
28#[derive(Debug, Clone, Serialize)]
29pub struct PluginDescriptor {
30 pub id: String,
31 pub name: String,
32 pub version: String,
33 pub kind: String,
34 pub capabilities: crate::manifest::PluginCapabilities,
35 pub manifest_path: String,
36 pub ui: crate::manifest::PluginUiContributions,
37 pub in_process: bool,
38}
39
40#[derive(Debug, Clone, Serialize)]
41pub struct RunPluginResult {
42 pub diagnostics: Vec<Diagnostic>,
43 pub output_paths: Vec<String>,
44 pub logs: Option<String>,
45 pub success: bool,
46}
47
48pub struct PluginHost {
49 workspace: PathBuf,
50 discovered: Vec<DiscoveredPlugin>,
51 validators: HashMap<String, Box<dyn ValidatorPlugin>>,
52 exporters: HashMap<String, Box<dyn ExporterPlugin>>,
53 workflows: HashMap<String, Box<dyn WorkflowPlugin>>,
54}
55
56impl PluginHost {
57 pub fn new(workspace: impl AsRef<Path>) -> Self {
58 Self {
59 workspace: workspace.as_ref().to_path_buf(),
60 discovered: Vec::new(),
61 validators: HashMap::new(),
62 exporters: HashMap::new(),
63 workflows: HashMap::new(),
64 }
65 }
66
67 pub fn discover(&mut self) -> Result<(), PluginHostError> {
68 self.discovered = discover_plugins(&self.workspace)?;
69 Ok(())
70 }
71
72 pub fn register_validator(&mut self, plugin: Box<dyn ValidatorPlugin>) {
73 self.validators.insert(plugin.id().to_string(), plugin);
74 }
75
76 pub fn register_exporter(&mut self, plugin: Box<dyn ExporterPlugin>) {
77 self.exporters.insert(plugin.id().to_string(), plugin);
78 }
79
80 pub fn register_workflow(&mut self, plugin: Box<dyn WorkflowPlugin>) {
81 self.workflows.insert(plugin.id().to_string(), plugin);
82 }
83
84 pub fn workspace(&self) -> &Path {
85 &self.workspace
86 }
87
88 pub fn discovered(&self) -> &[DiscoveredPlugin] {
89 &self.discovered
90 }
91
92 pub fn list_plugins(&self) -> Vec<PluginDescriptor> {
93 self.discovered
94 .iter()
95 .map(|p| {
96 let id = p.plugin_id().to_string();
97 PluginDescriptor {
98 id: id.clone(),
99 name: p.manifest.name.clone(),
100 version: p.manifest.version.clone(),
101 kind: p.manifest.kind.as_str().to_string(),
102 capabilities: p.manifest.capabilities.clone(),
103 manifest_path: p.manifest_path.display().to_string(),
104 ui: p.manifest.ui.clone(),
105 in_process: self.validators.contains_key(&id)
106 || self.exporters.contains_key(&id)
107 || self.workflows.contains_key(&id),
108 }
109 })
110 .collect()
111 }
112
113 fn find_plugin(&self, plugin_id: &str) -> Result<&DiscoveredPlugin, PluginHostError> {
114 self.discovered
115 .iter()
116 .find(|p| p.plugin_id() == plugin_id)
117 .ok_or_else(|| PluginHostError::NotFound(plugin_id.to_string()))
118 }
119
120 pub fn run_validate_plugin(
121 &self,
122 plugin_id: &str,
123 catalog: &OntologyCatalog,
124 ) -> Result<Vec<Diagnostic>, PluginHostError> {
125 let plugin = self.find_plugin(plugin_id)?;
126 if !plugin.manifest.capabilities.supports_validation() {
127 return Err(PluginHostError::UnsupportedAction(
128 plugin_id.to_string(),
129 "validate".into(),
130 ));
131 }
132 if let Some(v) = self.validators.get(plugin_id) {
133 return Ok(v.validate(catalog, &self.workspace));
134 }
135 if plugin.manifest.entry.is_some() {
136 let output = run_plugin_subprocess(
137 plugin,
138 SubprocessRequest {
139 action: "validate",
140 workspace: &self.workspace,
141 step: None,
142 extra_args: &[],
143 },
144 )?;
145 return Ok(wire_to_diagnostics(plugin_id, &self.workspace, output));
146 }
147 Err(PluginHostError::NotFound(format!("no runtime for plugin {plugin_id}")))
148 }
149
150 pub fn run_all_validators(&self, catalog: &OntologyCatalog) -> Vec<Diagnostic> {
151 let mut all = Vec::new();
152 for plugin in &self.discovered {
153 if !plugin.manifest.capabilities.supports_validation() {
154 continue;
155 }
156 let id = plugin.plugin_id();
157 match self.run_validate_plugin(id, catalog) {
158 Ok(mut diags) => all.append(&mut diags),
159 Err(err) => all.push(plugin_diagnostic(
160 id,
161 "plugin_error",
162 DiagnosticSeverity::Error,
163 err.to_string(),
164 self.workspace.clone(),
165 None,
166 )),
167 }
168 }
169 all
170 }
171
172 pub fn run_export_plugin(
173 &self,
174 plugin_id: &str,
175 catalog: &OntologyCatalog,
176 options: ExportOptions,
177 ) -> Result<RunPluginResult, PluginHostError> {
178 let plugin = self.find_plugin(plugin_id)?;
179 if !plugin.manifest.capabilities.export {
180 return Err(PluginHostError::UnsupportedAction(plugin_id.to_string(), "export".into()));
181 }
182 if let Some(e) = self.exporters.get(plugin_id) {
183 let paths = e
184 .export(catalog, &self.workspace, options)
185 .map_err(|e| PluginHostError::Export(e.to_string()))?;
186 return Ok(RunPluginResult {
187 diagnostics: Vec::new(),
188 output_paths: paths.iter().map(|p| p.display().to_string()).collect(),
189 logs: None,
190 success: true,
191 });
192 }
193 if plugin.manifest.entry.is_some() {
194 let output = run_plugin_subprocess(
195 plugin,
196 SubprocessRequest {
197 action: "export",
198 workspace: &self.workspace,
199 step: None,
200 extra_args: &[],
201 },
202 )?;
203 let diags = wire_to_diagnostics(plugin_id, &self.workspace, output.clone());
204 return Ok(RunPluginResult {
205 diagnostics: diags,
206 output_paths: output.output_paths,
207 logs: output.logs,
208 success: true,
209 });
210 }
211 Err(PluginHostError::NotFound(format!("no runtime for plugin {plugin_id}")))
212 }
213
214 pub fn run_workflow_plugin(
215 &self,
216 plugin_id: &str,
217 step: &str,
218 ) -> Result<RunPluginResult, PluginHostError> {
219 let plugin = self.find_plugin(plugin_id)?;
220 if plugin.manifest.kind != PluginKind::Workflow && plugin.manifest.kind != PluginKind::Build
221 {
222 return Err(PluginHostError::UnsupportedAction(
223 plugin_id.to_string(),
224 "workflow".into(),
225 ));
226 }
227 if let Some(w) = self.workflows.get(plugin_id) {
228 let result =
229 w.run(&self.workspace, WorkflowRequest { step: step.to_string(), dry_run: false });
230 return Ok(RunPluginResult {
231 diagnostics: result.diagnostics,
232 output_paths: Vec::new(),
233 logs: Some(result.logs),
234 success: result.success,
235 });
236 }
237 if plugin.manifest.entry.is_some() {
238 let output = run_plugin_subprocess(
239 plugin,
240 SubprocessRequest {
241 action: "workflow",
242 workspace: &self.workspace,
243 step: Some(step),
244 extra_args: &[],
245 },
246 )?;
247 let diags = wire_to_diagnostics(plugin_id, &self.workspace, output.clone());
248 return Ok(RunPluginResult {
249 diagnostics: diags,
250 output_paths: output.output_paths,
251 logs: output.logs,
252 success: output.exit_message.is_none(),
253 });
254 }
255 Err(PluginHostError::NotFound(format!("no runtime for plugin {plugin_id}")))
256 }
257
258 pub fn run_plugin_action(
259 &self,
260 plugin_id: &str,
261 action: &str,
262 catalog: Option<&OntologyCatalog>,
263 export_options: Option<ExportOptions>,
264 step: Option<&str>,
265 ) -> Result<RunPluginResult, PluginHostError> {
266 match action {
267 "validate" => {
268 let catalog = catalog.ok_or_else(|| {
269 PluginHostError::UnsupportedAction(
270 plugin_id.to_string(),
271 "validate (no catalog)".into(),
272 )
273 })?;
274 let diags = self.run_validate_plugin(plugin_id, catalog)?;
275 Ok(RunPluginResult {
276 diagnostics: diags,
277 output_paths: Vec::new(),
278 logs: None,
279 success: true,
280 })
281 }
282 "export" => {
283 let catalog = catalog.ok_or_else(|| {
284 PluginHostError::UnsupportedAction(
285 plugin_id.to_string(),
286 "export (no catalog)".into(),
287 )
288 })?;
289 let options =
290 export_options.unwrap_or_else(|| ExportOptions::markdown("plugin-out"));
291 self.run_export_plugin(plugin_id, catalog, options)
292 }
293 "workflow" => self.run_workflow_plugin(plugin_id, step.unwrap_or("qc")),
294 _ => Err(PluginHostError::UnsupportedAction(plugin_id.to_string(), action.into())),
295 }
296 }
297}
298
299fn wire_to_diagnostics(plugin_id: &str, workspace: &Path, output: PluginOutput) -> Vec<Diagnostic> {
300 output.diagnostics.into_iter().map(|d| d.into_diagnostic(plugin_id, workspace)).collect()
301}
302
303pub fn merge_plugin_diagnostics(base: &mut Vec<Diagnostic>, plugin: Vec<Diagnostic>) {
304 base.extend(plugin);
305}
306
307pub fn manifest_for_builtin(id: &str) -> Option<PluginManifest> {
308 let text = match id {
309 "ontocode.naming-validator" => include_str!("../fixtures/builtin-naming.toml"),
310 "ontocode.markdown-export" => include_str!("../fixtures/builtin-markdown.toml"),
311 "ontocode.shacl-validator" => include_str!("../fixtures/builtin-shacl.toml"),
312 _ => return None,
313 };
314 crate::manifest::parse_manifest(text).ok()
315}