use crate::utils::once::static_regex;
static_regex!(
VERSION_PATTERN,
r#"(?x) # verbose mode
^ # start of string
v? # optional 'v' prefix
(?<major>\d+) # major version number
(?: # non-capturing group for grouping the separator
\. # literal dot separator
(?<minor>\d+) # minor version number
)? # end of non-capturing group, optional
(?: # non-capturing group for grouping the separator
\. # literal dot separator
(?<patch>\d+) # patch version number
)? # end of non-capturing group, optional
$ # end of string
"#
);
#[derive(Eq)]
pub(crate) struct Version<'a> {
raw: &'a str,
major: u64,
minor: u64,
patch: u64,
}
impl<'a> Version<'a> {
pub(crate) fn parse(s: &'a str) -> anyhow::Result<Self> {
let captures = VERSION_PATTERN
.captures(s)
.ok_or_else(|| anyhow::anyhow!("invalid version format: {s}"))?;
let major = captures
.name("major")
.expect("impossible: missing required 'major' capture")
.as_str()
.parse()
.or_else(|e| anyhow::bail!("invalid major version in {s}: {e}"))?;
let minor = captures.name("minor").map_or(Ok(0), |m| {
m.as_str()
.parse()
.or_else(|e| anyhow::bail!("invalid minor version in {s}: {e}"))
})?;
let patch = captures.name("patch").map_or(Ok(0), |m| {
m.as_str()
.parse()
.or_else(|e| anyhow::bail!("invalid patch version in {s}: {e}"))
})?;
Ok(Self {
raw: s,
major,
minor,
patch,
})
}
pub(crate) fn raw(&self) -> &'a str {
self.raw
}
}
impl Ord for Version<'_> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch))
}
}
impl PartialOrd for Version<'_> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Version<'_> {
fn eq(&self, other: &Self) -> bool {
(self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
}
}
#[cfg(test)]
mod tests {
use super::Version;
#[test]
fn parse_valid_versions() {
let cases = [
("v1", 1, 0, 0),
("v1.2", 1, 2, 0),
("v1.2.3", 1, 2, 3),
("1", 1, 0, 0),
("1.2", 1, 2, 0),
("1.2.3", 1, 2, 3),
("v0.0.1", 0, 0, 1),
("0.0.1", 0, 0, 1),
("v10.20.30", 10, 20, 30),
("10.20.30", 10, 20, 30),
("0.0.0", 0, 0, 0),
("v0", 0, 0, 0),
("v0.0", 0, 0, 0),
("0", 0, 0, 0),
("0.0", 0, 0, 0),
("v000.0.1", 0, 0, 1),
("000.0.1", 0, 0, 1),
];
for (input, exp_major, exp_minor, exp_patch) in cases {
let version = Version::parse(input).unwrap();
assert_eq!(version.major, exp_major);
assert_eq!(version.minor, exp_minor);
assert_eq!(version.patch, exp_patch);
assert_eq!(version.raw(), input);
}
}
#[test]
fn parse_invalid_versions() {
let cases = [
"", "v", "v1.2.3.4", "nonsense", "v1.beta", ".1", ".v1", "v.1", "v1.", "v1.2.",
];
for input in cases {
assert!(Version::parse(input).is_err(),);
}
}
#[test]
fn compare_versions() {
let cases = [
("v1", "v1", std::cmp::Ordering::Equal),
("v1", "v1.0", std::cmp::Ordering::Equal),
("v1", "v1.0.0", std::cmp::Ordering::Equal),
("v1.0", "v1.0.0", std::cmp::Ordering::Equal),
("v1.2", "v1.2.0", std::cmp::Ordering::Equal),
("v1.2.3", "v1.2.3", std::cmp::Ordering::Equal),
("v1", "v2", std::cmp::Ordering::Less),
("v1.0", "v2.0", std::cmp::Ordering::Less),
("v1.0.0", "v2.0.0", std::cmp::Ordering::Less),
("v1.2", "v2.0", std::cmp::Ordering::Less),
("v1.2.3", "v2.0.0", std::cmp::Ordering::Less),
("v1.2", "v1.3", std::cmp::Ordering::Less),
("v1.2.3", "v1.3.0", std::cmp::Ordering::Less),
("v1.2.3", "v1.2.4", std::cmp::Ordering::Less),
("v2", "v1", std::cmp::Ordering::Greater),
("v2.0", "v1.0", std::cmp::Ordering::Greater),
("v2.0.0", "v1.0.0", std::cmp::Ordering::Greater),
("v2.0", "v1.2", std::cmp::Ordering::Greater),
("v2.0.0", "v1.2.3", std::cmp::Ordering::Greater),
("v1.3", "v1.2", std::cmp::Ordering::Greater),
("v1.3.0", "v1.2.3", std::cmp::Ordering::Greater),
("v1.2.4", "v1.2.3", std::cmp::Ordering::Greater),
];
for (v1_str, v2_str, expected_ordering) in cases {
let v1 = Version::parse(v1_str).unwrap();
let v2 = Version::parse(v2_str).unwrap();
assert_eq!(v1.cmp(&v2), expected_ordering,);
}
}
}