use std::{cmp, env, fmt, str};
pub fn crate_version() -> Version {
let major = env!("CARGO_PKG_VERSION_MAJOR").parse::<u16>().unwrap();
let minor = env!("CARGO_PKG_VERSION_MINOR").parse::<u16>().unwrap();
let patch = env!("CARGO_PKG_VERSION_PATCH").parse::<u16>().unwrap();
Version::MajorMinorPatch(major, minor, patch)
}
#[derive(Clone, Debug)]
pub enum Version {
Major(u16),
MajorMinor(u16, u16),
MajorMinorPatch(u16, u16, u16),
}
impl Version {
fn to_triplet(&self) -> (u16, u16, u16) {
match self {
Version::Major(a) => (*a, 0, 0),
Version::MajorMinor(a, b) => (*a, *b, 0),
Version::MajorMinorPatch(a, b, c) => (*a, *b, *c),
}
}
pub fn next_breaking(&self) -> Self {
use Version::*;
match self {
Major(0) => MajorMinorPatch(0, 1, 0),
Major(a) => MajorMinorPatch(a + 1, 0, 0),
MajorMinor(0, b) => MajorMinorPatch(0, b + 1, 0),
MajorMinor(a, _) => MajorMinorPatch(a + 1, 0, 0),
MajorMinorPatch(0, b, _) => MajorMinorPatch(0, b + 1, 0),
MajorMinorPatch(a, _, _) => MajorMinorPatch(a + 1, 0, 0),
}
}
pub fn cmp_to_range(&self, range: &VersionRange) -> cmp::Ordering {
#[allow(clippy::if_same_then_else)]
if *self < range.start {
cmp::Ordering::Less
} else if range.end < *self {
cmp::Ordering::Greater
} else if range.end == *self && !range.inclusive {
cmp::Ordering::Greater
} else {
cmp::Ordering::Equal
}
}
}
impl cmp::PartialEq for Version {
fn eq(&self, rhs: &Self) -> bool {
self.to_triplet() == rhs.to_triplet()
}
}
impl cmp::PartialOrd for Version {
fn partial_cmp(&self, rhs: &Self) -> Option<cmp::Ordering> {
self.to_triplet().partial_cmp(&rhs.to_triplet())
}
}
impl str::FromStr for Version {
type Err = ParseVersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut it = s.split('.');
let Some(major_str) = it.next() else {
return Err(ParseVersionError)
};
let major = major_str.parse::<u16>().map_err(|_| ParseVersionError)?;
let Some(minor_str) = it.next() else {
return Ok(Version::Major(major));
};
let minor = minor_str.parse::<u16>().map_err(|_| ParseVersionError)?;
let Some(patch_str) = it.next() else {
return Ok(Version::MajorMinor(major, minor))
};
let patch = patch_str.parse::<u16>().map_err(|_| ParseVersionError)?;
if it.next().is_none() {
Ok(Version::MajorMinorPatch(major, minor, patch))
} else {
Err(ParseVersionError)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ParseVersionError;
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Version::*;
match self {
Major(a) => write!(f, "{}.0.0", a),
MajorMinor(a, b) => write!(f, "{}.{}.0", a, b),
MajorMinorPatch(a, b, c) => write!(f, "{}.{}.{}", a, b, c),
}
}
}
#[derive(Clone, Debug)]
pub struct VersionRange {
pub start: Version,
pub end: Version,
pub inclusive: bool,
}
impl VersionRange {
pub fn non_breaking_from(start: &Version) -> Self {
Self {
start: start.clone(),
end: start.next_breaking(),
inclusive: false,
}
}
}
impl str::FromStr for VersionRange {
type Err = ParseVersionRangeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((start_str, end_str)) = s.split_once("..=") {
let Ok(start) = start_str.parse() else {
return Err(ParseVersionRangeError);
};
let Ok(end) = end_str.parse() else {
return Err(ParseVersionRangeError);
};
if start <= end {
Ok(Self {
start,
end,
inclusive: true,
})
} else {
Err(ParseVersionRangeError)
}
} else if let Some((start_str, end_str)) = s.split_once("..") {
let Ok(start) = start_str.parse() else {
return Err(ParseVersionRangeError);
};
let Ok(end) = end_str.parse() else {
return Err(ParseVersionRangeError);
};
if start < end {
Ok(Self {
start,
end,
inclusive: false,
})
} else {
Err(ParseVersionRangeError)
}
} else {
Err(ParseVersionRangeError)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ParseVersionRangeError;
impl fmt::Display for VersionRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}{}{}",
self.start,
if self.inclusive { "..=" } else { ".." },
self.end,
)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn version_equivalence() {
use Version::*;
assert_eq!(Major(1), Major(1));
assert_eq!(Major(1), MajorMinor(1, 0));
assert_eq!(Major(1), MajorMinorPatch(1, 0, 0));
assert_eq!(MajorMinor(1, 2), MajorMinor(1, 2));
assert_eq!(MajorMinor(1, 2), MajorMinorPatch(1, 2, 0));
assert_eq!(MajorMinorPatch(1, 2, 3), MajorMinorPatch(1, 2, 3));
}
#[test]
fn version_ordering() {
use Version::*;
assert!(Major(1) < Major(2));
assert!(Major(2) > Major(1));
assert!(MajorMinor(1, 2) < MajorMinor(1, 3));
assert!(MajorMinor(1, 3) > MajorMinor(1, 2));
assert!(MajorMinor(1, 2) < MajorMinor(2, 1));
assert!(MajorMinor(2, 1) > MajorMinor(1, 2));
assert!(Major(1) < MajorMinor(1, 2));
assert!(MajorMinor(1, 2) < Major(2));
assert!(MajorMinor(1, 2) > Major(1));
assert!(Major(2) > MajorMinor(1, 2));
assert!(MajorMinorPatch(1, 2, 3) < MajorMinorPatch(1, 2, 4));
assert!(MajorMinorPatch(1, 2, 4) > MajorMinorPatch(1, 2, 3));
assert!(MajorMinor(1, 2) < MajorMinorPatch(1, 2, 3));
assert!(Major(1) < MajorMinorPatch(1, 2, 3));
assert!(MajorMinorPatch(1, 2, 3) < MajorMinor(1, 3));
assert!(MajorMinorPatch(1, 2, 3) < Major(2));
assert!(MajorMinorPatch(1, 2, 3) > MajorMinor(1, 2));
assert!(MajorMinorPatch(1, 2, 3) > Major(1));
assert!(MajorMinor(1, 3) > MajorMinorPatch(1, 2, 3));
assert!(Major(2) > MajorMinorPatch(1, 2, 3));
}
#[test]
fn parse_good_version_strings() {
use Version::*;
assert!(match "1".parse::<Version>() {
Ok(Major(1)) => true,
_ => false,
});
assert!(match "1.2".parse::<Version>() {
Ok(MajorMinor(1, 2)) => true,
_ => false,
});
assert!(match "1.2.3".parse::<Version>() {
Ok(MajorMinorPatch(1, 2, 3)) => true,
_ => false,
});
assert!(match "123.456.789".parse::<Version>() {
Ok(MajorMinorPatch(123, 456, 789)) => true,
_ => false,
});
assert!(match "0.0.0".parse::<Version>() {
Ok(MajorMinorPatch(0, 0, 0)) => true,
_ => false,
});
}
#[test]
fn try_parsing_bad_version_strings() {
assert_eq!("".parse::<Version>(), Err(ParseVersionError));
assert_eq!("1 ".parse::<Version>(), Err(ParseVersionError));
assert_eq!(" 1".parse::<Version>(), Err(ParseVersionError));
assert_eq!(" ".parse::<Version>(), Err(ParseVersionError));
assert_eq!("1.".parse::<Version>(), Err(ParseVersionError));
assert_eq!("1.".parse::<Version>(), Err(ParseVersionError));
assert_eq!("1.2.".parse::<Version>(), Err(ParseVersionError));
assert_eq!("1.2.3.".parse::<Version>(), Err(ParseVersionError));
assert_eq!("1.2.3.4".parse::<Version>(), Err(ParseVersionError));
assert_eq!("whatever".parse::<Version>(), Err(ParseVersionError));
}
#[test]
fn display_version_string() {
use Version::*;
assert_eq!(format!("{}", Major(123)), "123.0.0");
assert_eq!(format!("{}", MajorMinor(123, 456)), "123.456.0");
assert_eq!(format!("{}", MajorMinorPatch(123, 456, 789)), "123.456.789");
}
#[test]
fn parse_good_version_ranges() {
use Version::*;
assert!(match "1..=1".parse() {
Ok(VersionRange {
start: Major(1),
end: Major(1),
inclusive: true,
}) => true,
bad => panic!("bad parse: {:#?}", bad),
});
assert!(match "1..=1.0.0".parse() {
Ok(VersionRange {
start: Major(1),
end: MajorMinorPatch(1, 0, 0),
inclusive: true,
}) => true,
bad => panic!("bad parse: {:#?}", bad),
});
assert!(match "1..1.0.1".parse() {
Ok(VersionRange {
start: Major(1),
end: MajorMinorPatch(1, 0, 1),
inclusive: false,
}) => true,
bad => panic!("bad parse: {:#?}", bad),
});
}
#[test]
fn try_parsing_bad_version_ranges() {
assert_eq!(
"1..1".parse::<VersionRange>().unwrap_err(),
ParseVersionRangeError
);
assert_eq!(
"1.2.3..1.2.2".parse::<VersionRange>().unwrap_err(),
ParseVersionRangeError
);
assert_eq!(
"1.2.3..=1.2.2".parse::<VersionRange>().unwrap_err(),
ParseVersionRangeError
);
}
#[test]
fn compare_version_against_range() {
use cmp::Ordering::*;
let point: VersionRange = "1.2.3..=1.2.3".parse().unwrap();
assert_eq!(
"1.2.2".parse::<Version>().unwrap().cmp_to_range(&point),
Less
);
assert_eq!(
"1.2.3".parse::<Version>().unwrap().cmp_to_range(&point),
Equal
);
assert_eq!(
"1.2.4".parse::<Version>().unwrap().cmp_to_range(&point),
Greater
);
let open: VersionRange = "1..2".parse().unwrap();
assert_eq!(
"0.65535.65535"
.parse::<Version>()
.unwrap()
.cmp_to_range(&open),
Less
);
assert_eq!(
"1.0.0".parse::<Version>().unwrap().cmp_to_range(&open),
Equal
);
assert_eq!(
"1.65535.65535"
.parse::<Version>()
.unwrap()
.cmp_to_range(&open),
Equal
);
assert_eq!(
"2.0.0".parse::<Version>().unwrap().cmp_to_range(&open),
Greater
);
assert_eq!(
"2.0.1".parse::<Version>().unwrap().cmp_to_range(&open),
Greater
);
let closed: VersionRange = "1..=2".parse().unwrap();
assert_eq!(
"0.65535.65535"
.parse::<Version>()
.unwrap()
.cmp_to_range(&closed),
Less
);
assert_eq!(
"1.0.0".parse::<Version>().unwrap().cmp_to_range(&closed),
Equal
);
assert_eq!(
"1.65535.65535"
.parse::<Version>()
.unwrap()
.cmp_to_range(&closed),
Equal
);
assert_eq!(
"2.0.0".parse::<Version>().unwrap().cmp_to_range(&closed),
Equal
);
assert_eq!(
"2.0.1".parse::<Version>().unwrap().cmp_to_range(&closed),
Greater
);
}
}