use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::ir::{Changeset, DiffNode};
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("comparator error in {comparator}: {message}")]
Comparator { comparator: String, message: String },
#[error("no comparator found for item: {0}")]
NoComparator(String),
#[error("csv error: {0}")]
Csv(String),
#[error("zip error: {0}")]
Zip(String),
#[error("tar error: {0}")]
Tar(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 ComparatorDescriptor {
pub sdk_version: String,
pub name: String,
#[serde(default)]
pub extensions: Vec<String>,
#[serde(default)]
pub media_types: Vec<String>,
#[serde(default)]
pub scope: ItemScope,
#[serde(default)]
pub handles_identical: bool,
}
impl ComparatorDescriptor {
pub fn new(name: impl Into<String>) -> Self {
Self {
sdk_version: SDK_VERSION.into(),
name: name.into(),
extensions: Vec::new(),
media_types: Vec::new(),
scope: ItemScope::Files,
handles_identical: false,
}
}
pub fn with_extensions(mut self, exts: Vec<String>) -> Self {
self.extensions = exts;
self
}
pub fn with_media_types(mut self, types: Vec<String>) -> Self {
self.media_types = types;
self
}
pub fn with_scope(mut self, scope: ItemScope) -> Self {
self.scope = scope;
self
}
pub fn with_handles_identical(mut self, handles: bool) -> Self {
self.handles_identical = handles;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TransformerDescriptor {
pub sdk_version: String,
pub name: String,
#[serde(default)]
pub match_types: Vec<String>,
#[serde(default)]
pub match_tags: Vec<String>,
#[serde(default)]
pub match_actions: Vec<String>,
#[serde(default)]
pub scope: TransformScope,
#[serde(default = "default_phase")]
pub suggested_phase: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub match_artifacts: Vec<ArtifactFormat>,
#[serde(default)]
pub node_shape: NodeShapeFilter,
}
fn default_phase() -> String {
"default".into()
}
impl TransformerDescriptor {
pub fn new(name: impl Into<String>) -> Self {
Self {
sdk_version: SDK_VERSION.into(),
name: name.into(),
match_types: Vec::new(),
match_tags: Vec::new(),
match_actions: Vec::new(),
scope: TransformScope::Node,
suggested_phase: "default".into(),
match_artifacts: Vec::new(),
node_shape: NodeShapeFilter::Any,
}
}
pub fn with_match_types(mut self, types: Vec<String>) -> Self {
self.match_types = types;
self
}
pub fn with_match_tags(mut self, tags: Vec<String>) -> Self {
self.match_tags = tags;
self
}
pub fn with_match_actions(mut self, actions: Vec<String>) -> Self {
self.match_actions = actions;
self
}
pub fn with_scope(mut self, scope: TransformScope) -> Self {
self.scope = scope;
self
}
pub fn with_match_artifacts(mut self, formats: Vec<ArtifactFormat>) -> Self {
self.match_artifacts = formats;
self
}
pub fn with_node_shape(mut self, shape: NodeShapeFilter) -> Self {
self.node_shape = shape;
self
}
}
#[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 Comparator: Send + Sync {
fn descriptor(&self) -> ComparatorDescriptor;
fn compare(&self, pair: &ItemPair, data: &dyn DataAccess) -> BinocResult<CompareResult>;
fn reopen(
&self,
_pair: &ItemPair,
_child_path: &str,
_data: &dyn DataAccess,
) -> BinocResult<ItemPair> {
Err(BinocError::Extract(format!(
"{} does not support reopen",
self.descriptor().name
)))
}
fn extract(
&self,
_node: &DiffNode,
_aspect: &str,
_data: &dyn DataAccess,
) -> Option<ExtractResult> {
None
}
}
pub trait Transformer: Send + Sync {
fn descriptor(&self) -> TransformerDescriptor;
fn transform(&self, node: DiffNode, data: &dyn DataAccess) -> TransformResult;
fn extract(
&self,
_node: &DiffNode,
_aspect: &str,
_data: &dyn DataAccess,
) -> Option<ExtractResult> {
None
}
}
pub trait Renderer: Send + Sync {
fn descriptor(&self) -> RendererDescriptor;
fn render(&self, changesets: &[Changeset], config: &serde_json::Value) -> BinocResult<String>;
}
#[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());
}
}