1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::ir::{Changeset, Diagnostic};
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("csv error: {0}")]
17 Csv(String),
18 #[error("zip error: {0}")]
19 Zip(String),
20 #[error("tar error: {0}")]
21 Tar(String),
22 #[error("gzip error: {0}")]
23 Gzip(String),
24 #[error("extract error: {0}")]
25 Extract(String),
26 #[error("path policy: {0}")]
27 PathPolicy(String),
28 #[error(
29 "SDK version mismatch: {plugin} (plugin '{name}') is not compatible with host SDK {host}"
30 )]
31 SdkVersion {
32 name: String,
33 plugin: String,
34 host: String,
35 },
36 #[error("{0}")]
37 Other(String),
38}
39
40pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
43
44const MIN_COMPATIBLE_MINOR: u64 = 1;
48
49pub fn check_sdk_compatibility(plugin_name: &str, plugin_version: &str) -> BinocResult<()> {
56 let host = parse_semver(SDK_VERSION);
57 let plugin = parse_semver(plugin_version);
58
59 let compatible = match (host, plugin) {
60 (Some((hm, hi, _)), Some((pm, pi, _))) if hm == 0 => {
61 hm == pm && pi >= MIN_COMPATIBLE_MINOR && pi <= hi
62 }
63 (Some((hm, hi, _)), Some((pm, pi, _))) => hm == pm && pi <= hi,
64 _ => false,
65 };
66
67 if compatible {
68 Ok(())
69 } else {
70 Err(BinocError::SdkVersion {
71 name: plugin_name.to_string(),
72 plugin: plugin_version.to_string(),
73 host: SDK_VERSION.to_string(),
74 })
75 }
76}
77
78fn parse_semver(v: &str) -> Option<(u64, u64, u64)> {
79 let mut parts = v.split('.');
80 let major = parts.next()?.parse().ok()?;
81 let minor = parts.next()?.parse().ok()?;
82 let patch = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
83 Some((major, minor, patch))
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88#[non_exhaustive]
89pub struct RendererDescriptor {
90 pub sdk_version: String,
91 pub name: String,
92 pub file_extension: String,
93}
94
95impl RendererDescriptor {
96 pub fn new(name: impl Into<String>, file_extension: impl Into<String>) -> Self {
97 Self {
98 sdk_version: SDK_VERSION.into(),
99 name: name.into(),
100 file_extension: file_extension.into(),
101 }
102 }
103}
104
105pub trait DataAccess: Send + Sync {
114 fn read_bytes(&self, item: &ItemRef) -> BinocResult<Vec<u8>>;
116
117 fn open_read(&self, item: &ItemRef) -> BinocResult<Box<dyn std::io::Read + Send>>;
119
120 fn local_path(&self, item: &ItemRef) -> BinocResult<PathBuf>;
123
124 fn provide(&self, logical_path: &str, content: &[u8]) -> BinocResult<ItemRef>;
127
128 fn workspace(&self) -> BinocResult<PathBuf>;
131
132 fn register_local(&self, physical: &Path, logical: &str) -> BinocResult<ItemRef>;
135
136 fn publish_artifact(
148 &self,
149 format: &ArtifactFormat,
150 subject: ArtifactSubject,
151 producer: &str,
152 data: &[u8],
153 ) -> BinocResult<ArtifactDescriptor>;
154
155 fn get_artifact(&self, descriptor: &ArtifactDescriptor) -> BinocResult<Option<Vec<u8>>>;
157
158 fn data_root(&self) -> BinocResult<PathBuf>;
163}
164
165pub trait Renderer: Send + Sync {
167 fn descriptor(&self) -> RendererDescriptor;
168
169 fn render(&self, changesets: &[Changeset], config: &serde_json::Value) -> BinocResult<String>;
170
171 fn diagnostics(&self, _changeset: &Changeset, _config: &serde_json::Value) -> Vec<Diagnostic> {
172 Vec::new()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn same_version_is_compatible() {
182 assert!(check_sdk_compatibility("test", SDK_VERSION).is_ok());
183 }
184
185 #[test]
186 fn patch_difference_is_compatible() {
187 let host = parse_semver(SDK_VERSION).unwrap();
188 let tweaked = format!("{}.{}.99", host.0, host.1);
189 assert!(check_sdk_compatibility("test", &tweaked).is_ok());
190 }
191
192 #[test]
193 fn older_minor_within_floor_is_compatible() {
194 let host = parse_semver(SDK_VERSION).unwrap();
195 if host.0 != 0 || host.1 < MIN_COMPATIBLE_MINOR {
196 return;
197 }
198 let oldest_ok = format!("0.{}.0", MIN_COMPATIBLE_MINOR);
199 assert!(check_sdk_compatibility("test", &oldest_ok).is_ok());
200 }
201
202 #[test]
203 fn older_minor_below_floor_rejected() {
204 if MIN_COMPATIBLE_MINOR == 0 {
205 return; }
207 let too_old = format!("0.{}.0", MIN_COMPATIBLE_MINOR - 1);
208 assert!(check_sdk_compatibility("test", &too_old).is_err());
209 }
210
211 #[test]
212 fn newer_minor_rejected_during_0x() {
213 let host = parse_semver(SDK_VERSION).unwrap();
214 if host.0 != 0 {
215 return;
216 }
217 let tweaked = format!("0.{}.0", host.1 + 1);
218 assert!(check_sdk_compatibility("test", &tweaked).is_err());
219 }
220
221 #[test]
222 fn garbage_version_rejected() {
223 assert!(check_sdk_compatibility("test", "not-a-version").is_err());
224 }
225}