use anyhow::Result;
use regex::Regex;
use std::sync::LazyLock;
#[derive(Debug, Clone)]
pub struct SemVer {
pub major: u64,
pub minor: u64,
pub patch: u64,
pub prerelease: Option<String>,
pub build_metadata: Option<String>,
}
impl SemVer {
pub fn is_prerelease(&self) -> bool {
self.prerelease.is_some()
}
}
impl PartialEq for SemVer {
fn eq(&self, other: &Self) -> bool {
self.major == other.major
&& self.minor == other.minor
&& self.patch == other.patch
&& self.prerelease == other.prerelease
}
}
impl Eq for SemVer {}
impl PartialOrd for SemVer {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SemVer {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.major
.cmp(&other.major)
.then(self.minor.cmp(&other.minor))
.then(self.patch.cmp(&other.patch))
.then(match (&self.prerelease, &other.prerelease) {
(Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (Some(a), Some(b)) => compare_prerelease(a, b),
(None, None) => std::cmp::Ordering::Equal,
})
}
}
pub(super) fn compare_prerelease(a: &str, b: &str) -> std::cmp::Ordering {
use std::cmp::Ordering;
let a_ids: Vec<&str> = a.split('.').collect();
let b_ids: Vec<&str> = b.split('.').collect();
for (ai, bi) in a_ids.iter().zip(b_ids.iter()) {
let ord = match (ai.parse::<u64>(), bi.parse::<u64>()) {
(Ok(an), Ok(bn)) => an.cmp(&bn), (Ok(_), Err(_)) => Ordering::Less, (Err(_), Ok(_)) => Ordering::Greater, (Err(_), Err(_)) => ai.cmp(bi), };
if ord != Ordering::Equal {
return ord;
}
}
a_ids.len().cmp(&b_ids.len())
}
static SEMVER_RE: LazyLock<Regex> =
LazyLock::new(|| crate::util::static_regex(r"^v?(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+(.+))?$"));
pub fn parse_semver(tag: &str) -> Result<SemVer> {
let caps = SEMVER_RE
.captures(tag)
.ok_or_else(|| anyhow::anyhow!("not a valid semver tag: {}", tag))?;
Ok(SemVer {
major: caps[1].parse()?,
minor: caps[2].parse()?,
patch: caps[3].parse()?,
prerelease: caps.get(4).map(|m| m.as_str().to_string()),
build_metadata: caps.get(5).map(|m| m.as_str().to_string()),
})
}
pub fn parse_semver_tag(tag: &str) -> Result<SemVer> {
if let Ok(sv) = parse_semver(tag) {
return Ok(sv);
}
static PREFIX_RE: LazyLock<Regex> =
LazyLock::new(|| crate::util::static_regex(r"[-_/](v?\d+\.\d+\.\d+(?:-[^+]+)?(?:\+.+)?)$"));
if let Some(caps) = PREFIX_RE.captures(tag) {
return parse_semver(&caps[1]);
}
anyhow::bail!("not a valid semver tag: {}", tag)
}