use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::ir::{Changeset, Diagnostic};
use crate::types::*;
pub type BinocResult<T> = Result<T, BinocError>;
#[derive(Debug, thiserror::Error)]
pub enum BinocError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("config error: {0}")]
Config(String),
#[error("csv error: {0}")]
Csv(String),
#[error("zip error: {0}")]
Zip(String),
#[error("tar error: {0}")]
Tar(String),
#[error("gzip error: {0}")]
Gzip(String),
#[error("extract error: {0}")]
Extract(String),
#[error("path policy: {0}")]
PathPolicy(String),
#[error(
"SDK version mismatch: {plugin} (plugin '{name}') is not compatible with host SDK {host}"
)]
SdkVersion {
name: String,
plugin: String,
host: String,
},
#[error("{0}")]
Other(String),
}
pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
const MIN_COMPATIBLE_MINOR: u64 = 1;
pub fn check_sdk_compatibility(plugin_name: &str, plugin_version: &str) -> BinocResult<()> {
let host = parse_semver(SDK_VERSION);
let plugin = parse_semver(plugin_version);
let compatible = match (host, plugin) {
(Some((hm, hi, _)), Some((pm, pi, _))) if hm == 0 => {
hm == pm && pi >= MIN_COMPATIBLE_MINOR && pi <= hi
}
(Some((hm, hi, _)), Some((pm, pi, _))) => hm == pm && pi <= hi,
_ => false,
};
if compatible {
Ok(())
} else {
Err(BinocError::SdkVersion {
name: plugin_name.to_string(),
plugin: plugin_version.to_string(),
host: SDK_VERSION.to_string(),
})
}
}
fn parse_semver(v: &str) -> Option<(u64, u64, u64)> {
let mut parts = v.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
Some((major, minor, patch))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RendererDescriptor {
pub sdk_version: String,
pub name: String,
pub file_extension: String,
}
impl RendererDescriptor {
pub fn new(name: impl Into<String>, file_extension: impl Into<String>) -> Self {
Self {
sdk_version: SDK_VERSION.into(),
name: name.into(),
file_extension: file_extension.into(),
}
}
}
pub trait DataAccess: Send + Sync {
fn read_bytes(&self, item: &ItemRef) -> BinocResult<Vec<u8>>;
fn open_read(&self, item: &ItemRef) -> BinocResult<Box<dyn std::io::Read + Send>>;
fn local_path(&self, item: &ItemRef) -> BinocResult<PathBuf>;
fn provide(&self, logical_path: &str, content: &[u8]) -> BinocResult<ItemRef>;
fn workspace(&self) -> BinocResult<PathBuf>;
fn register_local(&self, physical: &Path, logical: &str) -> BinocResult<ItemRef>;
fn publish_artifact(
&self,
format: &ArtifactFormat,
subject: ArtifactSubject,
producer: &str,
data: &[u8],
) -> BinocResult<ArtifactDescriptor>;
fn get_artifact(&self, descriptor: &ArtifactDescriptor) -> BinocResult<Option<Vec<u8>>>;
fn data_root(&self) -> BinocResult<PathBuf>;
}
pub trait Renderer: Send + Sync {
fn descriptor(&self) -> RendererDescriptor;
fn render(&self, changesets: &[Changeset], config: &serde_json::Value) -> BinocResult<String>;
fn diagnostics(&self, _changeset: &Changeset, _config: &serde_json::Value) -> Vec<Diagnostic> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_version_is_compatible() {
assert!(check_sdk_compatibility("test", SDK_VERSION).is_ok());
}
#[test]
fn patch_difference_is_compatible() {
let host = parse_semver(SDK_VERSION).unwrap();
let tweaked = format!("{}.{}.99", host.0, host.1);
assert!(check_sdk_compatibility("test", &tweaked).is_ok());
}
#[test]
fn older_minor_within_floor_is_compatible() {
let host = parse_semver(SDK_VERSION).unwrap();
if host.0 != 0 || host.1 < MIN_COMPATIBLE_MINOR {
return;
}
let oldest_ok = format!("0.{}.0", MIN_COMPATIBLE_MINOR);
assert!(check_sdk_compatibility("test", &oldest_ok).is_ok());
}
#[test]
fn older_minor_below_floor_rejected() {
if MIN_COMPATIBLE_MINOR == 0 {
return; }
let too_old = format!("0.{}.0", MIN_COMPATIBLE_MINOR - 1);
assert!(check_sdk_compatibility("test", &too_old).is_err());
}
#[test]
fn newer_minor_rejected_during_0x() {
let host = parse_semver(SDK_VERSION).unwrap();
if host.0 != 0 {
return;
}
let tweaked = format!("0.{}.0", host.1 + 1);
assert!(check_sdk_compatibility("test", &tweaked).is_err());
}
#[test]
fn garbage_version_rejected() {
assert!(check_sdk_compatibility("test", "not-a-version").is_err());
}
}