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 = "default_phase")]
156 pub suggested_phase: String,
157 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub match_artifacts: Vec<ArtifactFormat>,
161 #[serde(default)]
165 pub node_shape: NodeShapeFilter,
166}
167
168fn default_phase() -> String {
169 "default".into()
170}
171
172impl TransformerDescriptor {
173 pub fn new(name: impl Into<String>) -> Self {
174 Self {
175 sdk_version: SDK_VERSION.into(),
176 name: name.into(),
177 match_types: Vec::new(),
178 match_tags: Vec::new(),
179 match_actions: Vec::new(),
180 suggested_phase: "default".into(),
181 match_artifacts: Vec::new(),
182 node_shape: NodeShapeFilter::Any,
183 }
184 }
185
186 pub fn with_match_types(mut self, types: Vec<String>) -> Self {
187 self.match_types = types;
188 self
189 }
190
191 pub fn with_match_tags(mut self, tags: Vec<String>) -> Self {
192 self.match_tags = tags;
193 self
194 }
195
196 pub fn with_match_actions(mut self, actions: Vec<String>) -> Self {
197 self.match_actions = actions;
198 self
199 }
200
201 pub fn with_match_artifacts(mut self, formats: Vec<ArtifactFormat>) -> Self {
202 self.match_artifacts = formats;
203 self
204 }
205
206 pub fn with_node_shape(mut self, shape: NodeShapeFilter) -> Self {
207 self.node_shape = shape;
208 self
209 }
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214#[non_exhaustive]
215pub struct RendererDescriptor {
216 pub sdk_version: String,
217 pub name: String,
218 pub file_extension: String,
219}
220
221impl RendererDescriptor {
222 pub fn new(name: impl Into<String>, file_extension: impl Into<String>) -> Self {
223 Self {
224 sdk_version: SDK_VERSION.into(),
225 name: name.into(),
226 file_extension: file_extension.into(),
227 }
228 }
229}
230
231pub trait DataAccess: Send + Sync {
240 fn read_bytes(&self, item: &ItemRef) -> BinocResult<Vec<u8>>;
242
243 fn open_read(&self, item: &ItemRef) -> BinocResult<Box<dyn std::io::Read + Send>>;
245
246 fn local_path(&self, item: &ItemRef) -> BinocResult<PathBuf>;
249
250 fn provide(&self, logical_path: &str, content: &[u8]) -> BinocResult<ItemRef>;
253
254 fn workspace(&self) -> BinocResult<PathBuf>;
257
258 fn register_local(&self, physical: &Path, logical: &str) -> BinocResult<ItemRef>;
261
262 fn publish_artifact(
275 &self,
276 format: &ArtifactFormat,
277 subject: ArtifactSubject,
278 producer: &str,
279 data: &[u8],
280 ) -> BinocResult<ArtifactDescriptor>;
281
282 fn get_artifact(&self, descriptor: &ArtifactDescriptor) -> BinocResult<Option<Vec<u8>>>;
284
285 fn data_root(&self) -> BinocResult<PathBuf>;
290}
291
292pub trait Comparator: Send + Sync {
301 fn descriptor(&self) -> ComparatorDescriptor;
302
303 fn compare(&self, pair: &ItemPair, data: &dyn DataAccess) -> BinocResult<CompareResult>;
304
305 fn reopen(
313 &self,
314 _pair: &ItemPair,
315 _child_path: &str,
316 _data: &dyn DataAccess,
317 ) -> BinocResult<ItemPair> {
318 Err(BinocError::Extract(format!(
319 "{} does not support reopen",
320 self.descriptor().name
321 )))
322 }
323
324 fn extract(
326 &self,
327 _node: &DiffNode,
328 _aspect: &str,
329 _data: &dyn DataAccess,
330 ) -> Option<ExtractResult> {
331 None
332 }
333}
334
335pub trait Transformer: Send + Sync {
345 fn descriptor(&self) -> TransformerDescriptor;
346
347 fn transform(
348 &self,
349 node: DiffNode,
350 data: &dyn DataAccess,
351 config: &serde_json::Value,
352 ) -> TransformResult;
353
354 fn extract(
356 &self,
357 _node: &DiffNode,
358 _aspect: &str,
359 _data: &dyn DataAccess,
360 ) -> Option<ExtractResult> {
361 None
362 }
363}
364
365pub trait Renderer: Send + Sync {
367 fn descriptor(&self) -> RendererDescriptor;
368
369 fn render(&self, changesets: &[Changeset], config: &serde_json::Value) -> BinocResult<String>;
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn same_version_is_compatible() {
378 assert!(check_sdk_compatibility("test", SDK_VERSION).is_ok());
379 }
380
381 #[test]
382 fn patch_difference_is_compatible() {
383 let host = parse_semver(SDK_VERSION).unwrap();
384 let tweaked = format!("{}.{}.99", host.0, host.1);
385 assert!(check_sdk_compatibility("test", &tweaked).is_ok());
386 }
387
388 #[test]
389 fn older_minor_within_floor_is_compatible() {
390 let host = parse_semver(SDK_VERSION).unwrap();
391 if host.0 != 0 || host.1 < MIN_COMPATIBLE_MINOR {
392 return;
393 }
394 let oldest_ok = format!("0.{}.0", MIN_COMPATIBLE_MINOR);
395 assert!(check_sdk_compatibility("test", &oldest_ok).is_ok());
396 }
397
398 #[test]
399 fn older_minor_below_floor_rejected() {
400 if MIN_COMPATIBLE_MINOR == 0 {
401 return; }
403 let too_old = format!("0.{}.0", MIN_COMPATIBLE_MINOR - 1);
404 assert!(check_sdk_compatibility("test", &too_old).is_err());
405 }
406
407 #[test]
408 fn newer_minor_rejected_during_0x() {
409 let host = parse_semver(SDK_VERSION).unwrap();
410 if host.0 != 0 {
411 return;
412 }
413 let tweaked = format!("0.{}.0", host.1 + 1);
414 assert!(check_sdk_compatibility("test", &tweaked).is_err());
415 }
416
417 #[test]
418 fn garbage_version_rejected() {
419 assert!(check_sdk_compatibility("test", "not-a-version").is_err());
420 }
421}