use semver::{Version, VersionReq};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionSpec {
Digits(Vec<u64>),
Range(VersionReq),
Named(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionSpecParseError {
Empty,
Invalid,
}
impl fmt::Display for VersionSpecParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("version specification must not be empty"),
Self::Invalid => formatter.write_str("invalid version specification"),
}
}
}
impl std::error::Error for VersionSpecParseError {}
const VERSION_COMPONENT_COUNT: usize = 3;
impl VersionSpec {
pub fn is_concrete_version(&self) -> bool {
matches!(self, Self::Digits(_))
}
pub fn parse(input: &str) -> Result<Self, VersionSpecParseError> {
if input.trim().is_empty() {
return Err(VersionSpecParseError::Empty);
}
let input = input.trim();
if matches!(input, "devel" | "release") {
return Ok(Self::Named(input.to_owned()));
}
let components = input.split('.').collect::<Vec<_>>();
if components.iter().all(|component| {
!component.is_empty() && component.bytes().all(|byte| byte.is_ascii_digit())
}) {
if components.len() > VERSION_COMPONENT_COUNT {
return Err(VersionSpecParseError::Invalid);
}
let digits = components
.iter()
.map(|component| component.parse::<u64>())
.collect::<Result<Vec<_>, _>>()
.map_err(|_| VersionSpecParseError::Invalid)?;
return Ok(Self::Digits(digits));
}
let requirement = VersionReq::parse(input).map_err(|_| VersionSpecParseError::Invalid)?;
if input.contains('+')
|| requirement
.comparators
.iter()
.any(|comparator| !comparator.pre.is_empty())
{
return Err(VersionSpecParseError::Invalid);
}
Ok(Self::Range(requirement))
}
pub(super) fn matches(&self, version: &Version) -> bool {
match self {
Self::Digits(digits) => {
let version_components: [u64; VERSION_COMPONENT_COUNT] =
[version.major, version.minor, version.patch];
digits.len() <= version_components.len()
&& digits
.iter()
.zip(version_components)
.all(|(requested, installed)| *requested == installed)
}
Self::Range(requirement) => requirement.matches(version),
Self::Named(_) => false,
}
}
}
pub fn resolve_version<'a>(spec: &VersionSpec, installed: &'a [Version]) -> Option<&'a Version> {
installed
.iter()
.filter(|version| spec.matches(version))
.max()
}