use crate::{proto, version};
use hex::FromHex;
use thiserror::Error;
#[derive(Clone, Debug, PartialEq)]
pub struct Version {
version: semver::Version,
git_hash: Option<[u8; 20]>,
producer: Option<String>,
}
impl Version {
pub fn version(&self) -> &semver::Version {
&self.version
}
pub fn git_hash(&self) -> Option<&[u8; 20]> {
self.git_hash.as_ref()
}
pub fn producer(&self) -> Option<&str> {
self.producer.as_deref()
}
pub(crate) fn compatible(&self) -> Result<(), VersionError> {
let version = self.version();
let version_req = version::semver_req();
version_req
.matches(version)
.then_some(())
.ok_or_else(|| VersionError::Substrait(version.clone(), version_req))
}
}
#[derive(Debug, Error, PartialEq)]
pub enum VersionError {
#[error(
"git hash must be a lowercase hex ASCII string, 40 characters in length: (git hash: {0})"
)]
GitHash(String),
#[error("version must be specified")]
Missing,
#[error("substrait version incompatible (version: `{0}`, supported: `{1}`)")]
Substrait(semver::Version, semver::VersionReq),
}
impl TryFrom<proto::Version> for Version {
type Error = VersionError;
fn try_from(value: proto::Version) -> Result<Self, Self::Error> {
let proto::Version {
major_number,
minor_number,
patch_number,
git_hash,
producer,
} = value;
if major_number == u32::default()
&& minor_number == u32::default()
&& patch_number == u32::default()
{
return Err(VersionError::Missing);
}
if !git_hash.is_empty()
&& (git_hash.len() != 40
|| !git_hash.chars().all(|x| matches!(x, '0'..='9' | 'a'..='f')))
{
return Err(VersionError::GitHash(git_hash));
}
let version = Version {
version: semver::Version::new(major_number as _, minor_number as _, patch_number as _),
git_hash: (!git_hash.is_empty()).then(|| <[u8; 20]>::from_hex(git_hash).unwrap()),
producer: (!producer.is_empty()).then_some(producer),
};
version.compatible()?;
Ok(version)
}
}
impl From<Version> for proto::Version {
fn from(version: Version) -> Self {
let Version {
version,
git_hash,
producer,
} = version;
proto::Version {
major_number: version.major as _,
minor_number: version.minor as _,
patch_number: version.patch as _,
git_hash: git_hash.map(hex::encode).unwrap_or_default(),
producer: producer.unwrap_or_default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version() -> Result<(), VersionError> {
let version = proto::Version::default();
assert_eq!(Version::try_from(version), Err(VersionError::Missing));
let version = version::version();
Version::try_from(version)?;
Ok(())
}
#[test]
fn git_hash() {
let base = version::version();
let git_hash = String::from("short");
let version = proto::Version {
git_hash: git_hash.clone(),
..base.clone()
};
assert_eq!(
Version::try_from(version),
Err(VersionError::GitHash(git_hash))
);
let git_hash = String::from("2FD4E1C67A2D28FCED849EE1BB76E7391B93EB12");
let version = proto::Version {
git_hash: git_hash.clone(),
..base.clone()
};
assert_eq!(
Version::try_from(version),
Err(VersionError::GitHash(git_hash))
);
let git_hash = String::from("2fd4e1c67a2d28fced849ee1bb76e7391b93eb1g");
let version = proto::Version {
git_hash: git_hash.clone(),
..base.clone()
};
assert_eq!(
Version::try_from(version),
Err(VersionError::GitHash(git_hash))
);
let git_hash = String::from("2fd4e1c67a2d28fced849ee1bb76e7391b93eb1å");
let version = proto::Version {
git_hash: git_hash.clone(),
..base.clone()
};
assert_eq!(
Version::try_from(version),
Err(VersionError::GitHash(git_hash))
);
let git_hash = String::from("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12");
let version = proto::Version { git_hash, ..base };
assert!(Version::try_from(version).is_ok());
}
#[test]
fn producer() -> Result<(), VersionError> {
let version = proto::Version {
producer: String::from(""),
..version::version()
};
let version = Version::try_from(version)?;
assert!(version.producer.is_none());
Ok(())
}
#[test]
fn convert() -> Result<(), VersionError> {
let version = version::version();
assert_eq!(
proto::Version::from(Version::try_from(version.clone())?),
version
);
Ok(())
}
#[test]
fn compatible() -> Result<(), VersionError> {
let _version = Version::try_from(version::version())?;
let mut version = version::version();
version.major_number += 1;
let version = Version::try_from(version);
matches!(version, Err(VersionError::Substrait(_, _)));
let mut version = version::version();
version.minor_number += 1;
let version = Version::try_from(version);
matches!(version, Err(VersionError::Substrait(_, _)));
let mut version = version::version();
version.patch_number += 1;
let _version = Version::try_from(version)?;
Ok(())
}
}