Skip to main content

ontocore_plugin/
protocol.rs

1use ontocore_core::{
2    validate_workspace_scope, Diagnostic, DiagnosticCode, DiagnosticSeverity, SourceLocation,
3};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6
7/// JSON wire format returned by subprocess plugins on stdout.
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct PluginOutput {
10    #[serde(default)]
11    pub diagnostics: Vec<PluginDiagnosticWire>,
12    #[serde(default)]
13    pub output_paths: Vec<String>,
14    #[serde(default)]
15    pub logs: Option<String>,
16    #[serde(default)]
17    pub exit_message: Option<String>,
18    /// Optional HTML payload for UI view contributions.
19    #[serde(default)]
20    pub view_html: Option<String>,
21    /// Non-fatal security violations (e.g. rejected paths) detected by the host.
22    #[serde(default)]
23    pub violations: Vec<String>,
24    /// Provider payload (reasoner / query / refactor / graph) — additive in SDK 1.0.
25    #[serde(default)]
26    pub result: Option<serde_json::Value>,
27    #[serde(default)]
28    pub columns: Option<Vec<String>>,
29    #[serde(default)]
30    pub rows: Option<Vec<Vec<String>>>,
31    #[serde(default)]
32    pub unsatisfiable: Option<Vec<String>>,
33    #[serde(default)]
34    pub affected_iris: Option<Vec<String>>,
35    #[serde(default)]
36    pub root_iris: Option<Vec<String>>,
37    #[serde(default)]
38    pub graph_kind: Option<String>,
39    #[serde(default)]
40    pub hints: Option<Vec<String>>,
41    #[serde(default)]
42    pub profile: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct PluginDiagnosticWire {
47    pub code: String,
48    pub severity: String,
49    pub message: String,
50    pub file: String,
51    #[serde(default)]
52    pub line: Option<u64>,
53    #[serde(default)]
54    pub column: Option<u64>,
55    #[serde(default)]
56    pub entity_iri: Option<String>,
57}
58
59impl PluginDiagnosticWire {
60    pub fn into_diagnostic(self, plugin_id: &str, workspace: &Path) -> Diagnostic {
61        let requested = PathBuf::from(&self.file);
62        let (file, message) = match validate_workspace_scope(&requested, workspace) {
63            Ok(file) => (file, self.message),
64            Err(err) => (
65                workspace.to_path_buf(),
66                format!(
67                    "{} (rejected plugin diagnostic file path '{}': {})",
68                    self.message, self.file, err
69                ),
70            ),
71        };
72        Diagnostic {
73            code: DiagnosticCode::PluginViolation,
74            severity: match self.severity.as_str() {
75                "error" => DiagnosticSeverity::Error,
76                "warning" => DiagnosticSeverity::Warning,
77                _ => DiagnosticSeverity::Info,
78            },
79            message,
80            file,
81            range: SourceLocation {
82                line: self.line,
83                column: self.column,
84                start_byte: None,
85                end_byte: None,
86            },
87            entity_iri: self.entity_iri,
88            quick_fix: None,
89            plugin_id: Some(plugin_id.to_string()),
90            plugin_code: Some(self.code),
91        }
92    }
93}
94
95pub fn jail_output_paths(
96    plugin_id: &str,
97    workspace: &Path,
98    mut output: PluginOutput,
99) -> PluginOutput {
100    let mut jailed = Vec::new();
101    for p in output.output_paths.drain(..) {
102        let requested = PathBuf::from(&p);
103        match validate_workspace_scope(&requested, workspace) {
104            Ok(path) => jailed.push(path.display().to_string()),
105            Err(err) => output
106                .violations
107                .push(format!("rejected plugin output path '{p}' for {plugin_id}: {err}")),
108        }
109    }
110    output.output_paths = jailed;
111    output
112}
113
114pub fn plugin_diagnostic(
115    plugin_id: &str,
116    code: &str,
117    severity: DiagnosticSeverity,
118    message: impl Into<String>,
119    file: PathBuf,
120    entity_iri: Option<String>,
121) -> Diagnostic {
122    Diagnostic {
123        code: DiagnosticCode::PluginViolation,
124        severity,
125        message: message.into(),
126        file,
127        range: SourceLocation::default(),
128        entity_iri,
129        quick_fix: None,
130        plugin_id: Some(plugin_id.to_string()),
131        plugin_code: Some(code.to_string()),
132    }
133}