1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::ir::{Changeset, DiffNode};
6use crate::types::*;
7
8pub type BinocResult<T> = Result<T, BinocError>;
9
10#[derive(Debug, thiserror::Error)]
11pub enum BinocError {
12 #[error("IO error: {0}")]
13 Io(#[from] std::io::Error),
14 #[error("config error: {0}")]
15 Config(String),
16 #[error("comparator error in {comparator}: {message}")]
17 Comparator { comparator: String, message: String },
18 #[error("no comparator found for item: {0}")]
19 NoComparator(String),
20 #[error("csv error: {0}")]
21 Csv(String),
22 #[error("zip error: {0}")]
23 Zip(String),
24 #[error("tar error: {0}")]
25 Tar(String),
26 #[error("extract error: {0}")]
27 Extract(String),
28 #[error("path policy: {0}")]
29 PathPolicy(String),
30 #[error(
31 "SDK version mismatch: {plugin} (plugin '{name}') is not compatible with host SDK {host}"
32 )]
33 SdkVersion {
34 name: String,
35 plugin: String,
36 host: String,
37 },
38 #[error("{0}")]
39 Other(String),
40}
41
42pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
45
46const MIN_COMPATIBLE_MINOR: u64 = 1;
50
51pub fn check_sdk_compatibility(plugin_name: &str, plugin_version: &str) -> BinocResult<()> {
58 let host = parse_semver(SDK_VERSION);
59 let plugin = parse_semver(plugin_version);
60
61 let compatible = match (host, plugin) {
62 (Some((hm, hi, _)), Some((pm, pi, _))) if hm == 0 => {
63 hm == pm && pi >= MIN_COMPATIBLE_MINOR && pi <= hi
64 }
65 (Some((hm, hi, _)), Some((pm, pi, _))) => hm == pm && pi <= hi,
66 _ => false,
67 };
68
69 if compatible {
70 Ok(())
71 } else {
72 Err(BinocError::SdkVersion {
73 name: plugin_name.to_string(),
74 plugin: plugin_version.to_string(),
75 host: SDK_VERSION.to_string(),
76 })
77 }
78}
79
80fn parse_semver(v: &str) -> Option<(u64, u64, u64)> {
81 let mut parts = v.split('.');
82 let major = parts.next()?.parse().ok()?;
83 let minor = parts.next()?.parse().ok()?;
84 let patch = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
85 Some((major, minor, patch))
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
91#[non_exhaustive]
92pub struct ComparatorDescriptor {
93 pub sdk_version: String,
94 pub name: String,
95 #[serde(default)]
96 pub extensions: Vec<String>,
97 #[serde(default)]
98 pub media_types: Vec<String>,
99 #[serde(default)]
100 pub scope: ItemScope,
101 #[serde(default)]
102 pub handles_identical: bool,
103}
104
105impl ComparatorDescriptor {
106 pub fn new(name: impl Into<String>) -> Self {
107 Self {
108 sdk_version: SDK_VERSION.into(),
109 name: name.into(),
110 extensions: Vec::new(),
111 media_types: Vec::new(),
112 scope: ItemScope::Files,
113 handles_identical: false,
114 }
115 }
116
117 pub fn with_extensions(mut self, exts: Vec<String>) -> Self {
118 self.extensions = exts;
119 self
120 }
121
122 pub fn with_media_types(mut self, types: Vec<String>) -> Self {
123 self.media_types = types;
124 self
125 }
126
127 pub fn with_scope(mut self, scope: ItemScope) -> Self {
128 self.scope = scope;
129 self
130 }
131
132 pub fn with_handles_identical(mut self, handles: bool) -> Self {
133 self.handles_identical = handles;
134 self
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
145#[non_exhaustive]
146pub struct TransformerDescriptor {
147 pub sdk_version: String,
148 pub name: String,
149 #[serde(default)]
150 pub match_types: Vec<String>,
151 #[serde(default)]
152 pub match_tags: Vec<String>,
153 #[serde(default)]
154 pub match_actions: Vec<String>,
155 #[serde(default)]
156 pub scope: TransformScope,
157 #[serde(default = "default_phase")]
158 pub suggested_phase: String,
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
162 pub match_artifacts: Vec<ArtifactFormat>,
163 #[serde(default)]
167 pub node_shape: NodeShapeFilter,
168}
169
170fn default_phase() -> String {
171 "default".into()
172}
173
174impl TransformerDescriptor {
175 pub fn new(name: impl Into<String>) -> Self {
176 Self {
177 sdk_version: SDK_VERSION.into(),
178 name: name.into(),
179 match_types: Vec::new(),
180 match_tags: Vec::new(),
181 match_actions: Vec::new(),
182 scope: TransformScope::Node,
183 suggested_phase: "default".into(),
184 match_artifacts: Vec::new(),
185 node_shape: NodeShapeFilter::Any,
186 }
187 }
188
189 pub fn with_match_types(mut self, types: Vec<String>) -> Self {
190 self.match_types = types;
191 self
192 }
193
194 pub fn with_match_tags(mut self, tags: Vec<String>) -> Self {
195 self.match_tags = tags;
196 self
197 }
198
199 pub fn with_match_actions(mut self, actions: Vec<String>) -> Self {
200 self.match_actions = actions;
201 self
202 }
203
204 pub fn with_scope(mut self, scope: TransformScope) -> Self {
205 self.scope = scope;
206 self
207 }
208
209 pub fn with_match_artifacts(mut self, formats: Vec<ArtifactFormat>) -> Self {
210 self.match_artifacts = formats;
211 self
212 }
213
214 pub fn with_node_shape(mut self, shape: NodeShapeFilter) -> Self {
215 self.node_shape = shape;
216 self
217 }
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222#[non_exhaustive]
223pub struct RendererDescriptor {
224 pub sdk_version: String,
225 pub name: String,
226 pub file_extension: String,
227}
228
229impl RendererDescriptor {
230 pub fn new(name: impl Into<String>, file_extension: impl Into<String>) -> Self {
231 Self {
232 sdk_version: SDK_VERSION.into(),
233 name: name.into(),
234 file_extension: file_extension.into(),
235 }
236 }
237}
238
239pub trait DataAccess: Send + Sync {
248 fn read_bytes(&self, item: &ItemRef) -> BinocResult<Vec<u8>>;
250
251 fn open_read(&self, item: &ItemRef) -> BinocResult<Box<dyn std::io::Read + Send>>;
253
254 fn local_path(&self, item: &ItemRef) -> BinocResult<PathBuf>;
257
258 fn provide(&self, logical_path: &str, content: &[u8]) -> BinocResult<ItemRef>;
261
262 fn workspace(&self) -> BinocResult<PathBuf>;
265
266 fn register_local(&self, physical: &Path, logical: &str) -> BinocResult<ItemRef>;
269
270 fn publish_artifact(
283 &self,
284 format: &ArtifactFormat,
285 subject: ArtifactSubject,
286 producer: &str,
287 data: &[u8],
288 ) -> BinocResult<ArtifactDescriptor>;
289
290 fn get_artifact(&self, descriptor: &ArtifactDescriptor) -> BinocResult<Option<Vec<u8>>>;
292
293 fn data_root(&self) -> BinocResult<PathBuf>;
298}
299
300pub trait Comparator: Send + Sync {
309 fn descriptor(&self) -> ComparatorDescriptor;
310
311 fn compare(&self, pair: &ItemPair, data: &dyn DataAccess) -> BinocResult<CompareResult>;
312
313 fn reopen(
321 &self,
322 _pair: &ItemPair,
323 _child_path: &str,
324 _data: &dyn DataAccess,
325 ) -> BinocResult<ItemPair> {
326 Err(BinocError::Extract(format!(
327 "{} does not support reopen",
328 self.descriptor().name
329 )))
330 }
331
332 fn extract(
334 &self,
335 _node: &DiffNode,
336 _aspect: &str,
337 _data: &dyn DataAccess,
338 ) -> Option<ExtractResult> {
339 None
340 }
341}
342
343pub trait Transformer: Send + Sync {
348 fn descriptor(&self) -> TransformerDescriptor;
349
350 fn transform(&self, node: DiffNode, data: &dyn DataAccess) -> TransformResult;
351
352 fn extract(
354 &self,
355 _node: &DiffNode,
356 _aspect: &str,
357 _data: &dyn DataAccess,
358 ) -> Option<ExtractResult> {
359 None
360 }
361}
362
363pub trait Renderer: Send + Sync {
365 fn descriptor(&self) -> RendererDescriptor;
366
367 fn render(&self, changesets: &[Changeset], config: &serde_json::Value) -> BinocResult<String>;
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn same_version_is_compatible() {
376 assert!(check_sdk_compatibility("test", SDK_VERSION).is_ok());
377 }
378
379 #[test]
380 fn patch_difference_is_compatible() {
381 let host = parse_semver(SDK_VERSION).unwrap();
382 let tweaked = format!("{}.{}.99", host.0, host.1);
383 assert!(check_sdk_compatibility("test", &tweaked).is_ok());
384 }
385
386 #[test]
387 fn older_minor_within_floor_is_compatible() {
388 let host = parse_semver(SDK_VERSION).unwrap();
389 if host.0 != 0 || host.1 < MIN_COMPATIBLE_MINOR {
390 return;
391 }
392 let oldest_ok = format!("0.{}.0", MIN_COMPATIBLE_MINOR);
393 assert!(check_sdk_compatibility("test", &oldest_ok).is_ok());
394 }
395
396 #[test]
397 fn older_minor_below_floor_rejected() {
398 if MIN_COMPATIBLE_MINOR == 0 {
399 return; }
401 let too_old = format!("0.{}.0", MIN_COMPATIBLE_MINOR - 1);
402 assert!(check_sdk_compatibility("test", &too_old).is_err());
403 }
404
405 #[test]
406 fn newer_minor_rejected_during_0x() {
407 let host = parse_semver(SDK_VERSION).unwrap();
408 if host.0 != 0 {
409 return;
410 }
411 let tweaked = format!("0.{}.0", host.1 + 1);
412 assert!(check_sdk_compatibility("test", &tweaked).is_err());
413 }
414
415 #[test]
416 fn garbage_version_rejected() {
417 assert!(check_sdk_compatibility("test", "not-a-version").is_err());
418 }
419}