use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl Version {
pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
}
pub const fn major_only(major: u32) -> Self {
Self::new(major, 0, 0)
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl FromStr for Version {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.trim().split(['.', '-', '+']);
let major = parse_component(parts.next())?;
let minor = match parts.next() {
Some(part) => parse_component(Some(part))?,
None => 0,
};
let patch = match parts.next() {
Some(part) => parse_component(Some(part))?,
None => 0,
};
Ok(Version::new(major, minor, patch))
}
}
fn parse_component(part: Option<&str>) -> Result<u32, ParseError> {
part.filter(|p| !p.is_empty())
.and_then(|p| p.parse().ok())
.ok_or(ParseError)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VersionRange {
pub min: Version,
pub max: Option<Version>,
}
impl VersionRange {
pub const fn exact(version: Version) -> Self {
Self {
min: version,
max: None,
}
}
pub fn covers_feature(&self, introduced: Version) -> bool {
self.min >= introduced
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseError;
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid Julia version specifier")
}
}
impl std::error::Error for ParseError {}
pub fn parse_compat(spec: &str) -> Result<VersionRange, ParseError> {
let mut overall: Option<VersionRange> = None;
for clause in spec.split(',') {
let clause = clause.trim();
if clause.is_empty() {
continue;
}
let range = parse_clause(clause)?;
overall = Some(match overall {
None => range,
Some(acc) => merge(acc, range),
});
}
overall.ok_or(ParseError)
}
fn merge(a: VersionRange, b: VersionRange) -> VersionRange {
let max = match (a.max, b.max) {
(Some(x), Some(y)) => Some(x.max(y)),
_ => None,
};
VersionRange {
min: a.min.min(b.min),
max,
}
}
fn parse_clause(clause: &str) -> Result<VersionRange, ParseError> {
if let Some((lo, hi)) = clause.split_once(" - ") {
let min: Version = lo.trim().parse()?;
let hi: Version = hi.trim().parse()?;
return Ok(VersionRange {
min,
max: Some(next_patch(hi)),
});
}
if let Some(rest) = clause.strip_prefix('~') {
let v: Version = rest.trim().parse()?;
return Ok(VersionRange {
min: v,
max: Some(next_minor(v)),
});
}
if let Some(rest) = clause.strip_prefix('=') {
let v: Version = rest.trim().parse()?;
return Ok(VersionRange {
min: v,
max: Some(next_patch(v)),
});
}
let rest = clause.strip_prefix('^').unwrap_or(clause);
let v: Version = rest.trim().parse()?;
Ok(VersionRange {
min: v,
max: Some(caret_ceiling(v)),
})
}
fn caret_ceiling(v: Version) -> Version {
if v.major != 0 {
Version::new(v.major + 1, 0, 0)
} else if v.minor != 0 {
Version::new(0, v.minor + 1, 0)
} else {
Version::new(0, 0, v.patch + 1)
}
}
fn next_minor(v: Version) -> Version {
Version::new(v.major, v.minor + 1, 0)
}
fn next_patch(v: Version) -> Version {
Version::new(v.major, v.minor, v.patch + 1)
}
#[cfg(test)]
mod tests {
use super::*;
fn v(major: u32, minor: u32, patch: u32) -> Version {
Version::new(major, minor, patch)
}
#[test]
fn parses_and_defaults_version_components() {
assert_eq!("1".parse::<Version>().unwrap(), v(1, 0, 0));
assert_eq!("1.6".parse::<Version>().unwrap(), v(1, 6, 0));
assert_eq!("1.6.2".parse::<Version>().unwrap(), v(1, 6, 2));
assert_eq!(" 1.11 ".parse::<Version>().unwrap(), v(1, 11, 0));
assert_eq!("1.12.0-rc1".parse::<Version>().unwrap(), v(1, 12, 0));
}
#[test]
fn rejects_non_numeric_versions() {
assert!("".parse::<Version>().is_err());
assert!("x".parse::<Version>().is_err());
assert!("1.x".parse::<Version>().is_err());
}
#[test]
fn version_orders_field_by_field() {
assert!(v(1, 6, 0) < v(1, 11, 0));
assert!(v(1, 11, 0) > v(1, 6, 5));
assert!(v(2, 0, 0) > v(1, 99, 99));
assert_eq!(v(1, 6, 0), "1.6".parse().unwrap());
}
#[test]
fn bare_version_is_caret() {
let r = parse_compat("1.6").unwrap();
assert_eq!(r.min, v(1, 6, 0));
assert_eq!(r.max, Some(v(2, 0, 0)));
}
#[test]
fn caret_ceiling_follows_first_nonzero() {
assert_eq!(parse_compat("^1.2.3").unwrap().max, Some(v(2, 0, 0)));
assert_eq!(parse_compat("0.3").unwrap().max, Some(v(0, 4, 0)));
assert_eq!(parse_compat("0.0.4").unwrap().max, Some(v(0, 0, 5)));
}
#[test]
fn tilde_bumps_minor() {
let r = parse_compat("~1.6").unwrap();
assert_eq!(r.min, v(1, 6, 0));
assert_eq!(r.max, Some(v(1, 7, 0)));
}
#[test]
fn equality_pins_one_patch() {
let r = parse_compat("=1.6.2").unwrap();
assert_eq!(r.min, v(1, 6, 2));
assert_eq!(r.max, Some(v(1, 6, 3)));
}
#[test]
fn hyphen_range_is_inclusive() {
let r = parse_compat("1.6 - 1.11").unwrap();
assert_eq!(r.min, v(1, 6, 0));
assert_eq!(r.max, Some(v(1, 11, 1)));
}
#[test]
fn comma_union_takes_lowest_floor_highest_ceiling() {
let r = parse_compat("1.6, 1.10").unwrap();
assert_eq!(r.min, v(1, 6, 0));
assert_eq!(r.max, Some(v(2, 0, 0)));
}
#[test]
fn empty_or_all_blank_spec_fails() {
assert!(parse_compat("").is_err());
assert!(parse_compat(" , ").is_err());
}
#[test]
fn covers_feature_checks_the_floor() {
let range = parse_compat("1.6").unwrap();
assert!(!range.covers_feature(v(1, 11, 0)));
assert!(range.covers_feature(v(1, 6, 0)));
assert!(range.covers_feature(v(1, 0, 0)));
let exact = VersionRange::exact(v(1, 11, 0));
assert!(exact.covers_feature(v(1, 11, 0)));
assert!(!exact.covers_feature(v(1, 12, 0)));
}
}