use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum BumpType {
Patch,
Minor,
Major,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemVer {
pub major: u64,
pub minor: u64,
pub patch: u64,
pub has_v_prefix: bool,
}
impl SemVer {
pub fn bump(&self, bump: BumpType) -> SemVer {
match bump {
BumpType::Major => SemVer {
major: self.major + 1,
minor: 0,
patch: 0,
has_v_prefix: self.has_v_prefix,
},
BumpType::Minor => SemVer {
major: self.major,
minor: self.minor + 1,
patch: 0,
has_v_prefix: self.has_v_prefix,
},
BumpType::Patch => SemVer {
major: self.major,
minor: self.minor,
patch: self.patch + 1,
has_v_prefix: self.has_v_prefix,
},
}
}
}
impl fmt::Display for SemVer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.has_v_prefix {
write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
} else {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
}
pub fn parse_semver(version: &str) -> Option<SemVer> {
if !validate_semver(version) {
return None;
}
let has_v_prefix = version.starts_with('v');
let s = version.strip_prefix('v').unwrap_or(version);
let core = s.split(['+', '-']).next().unwrap_or(s);
let parts: Vec<&str> = core.split('.').collect();
if parts.len() != 3 {
return None;
}
let major = parts[0].parse::<u64>().ok()?;
let minor = parts[1].parse::<u64>().ok()?;
let patch = parts[2].parse::<u64>().ok()?;
Some(SemVer {
major,
minor,
patch,
has_v_prefix,
})
}
pub fn validate_semver(version: &str) -> bool {
let s = version.strip_prefix('v').unwrap_or(version);
if s.is_empty() {
return false;
}
let (s, _build) = match s.split_once('+') {
Some((core, b)) => {
if b.is_empty()
|| !b
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
{
return false;
}
(core, Some(b))
}
None => (s, None),
};
let (core, _pre) = match s.split_once('-') {
Some((c, p)) => {
if p.is_empty()
|| !p
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
{
return false;
}
(c, Some(p))
}
None => (s, None),
};
let parts: Vec<&str> = core.split('.').collect();
if parts.len() != 3 {
return false;
}
for part in parts {
if part.is_empty() || !part.chars().all(|c| c.is_ascii_digit()) {
return false;
}
if part.len() > 1 && part.starts_with('0') {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_semver() {
assert!(validate_semver("1.0.0"));
assert!(validate_semver("v1.0.0"));
assert!(validate_semver("v0.1.0"));
assert!(validate_semver("v1.2.3-alpha.1"));
assert!(validate_semver("v1.2.3+build.42"));
assert!(validate_semver("v1.2.3-rc.1+build.1"));
}
#[test]
fn test_invalid_semver() {
assert!(!validate_semver(""));
assert!(!validate_semver("v"));
assert!(!validate_semver("1.0"));
assert!(!validate_semver("v1.0"));
assert!(!validate_semver("1.0.0.0"));
assert!(!validate_semver("v01.0.0"));
assert!(!validate_semver("latest"));
assert!(!validate_semver("v1.0.0-"));
}
#[test]
fn test_parse_semver() {
let v1 = parse_semver("v1.2.3").expect("valid");
assert_eq!(
v1,
SemVer {
major: 1,
minor: 2,
patch: 3,
has_v_prefix: true
}
);
assert_eq!(v1.to_string(), "v1.2.3");
let v2 = parse_semver("0.4.12").expect("valid");
assert_eq!(
v2,
SemVer {
major: 0,
minor: 4,
patch: 12,
has_v_prefix: false
}
);
assert_eq!(v2.to_string(), "0.4.12");
let v3 = parse_semver("v2.0.0-rc.1+build.5").expect("valid");
assert_eq!(
v3,
SemVer {
major: 2,
minor: 0,
patch: 0,
has_v_prefix: true
}
);
assert!(parse_semver("not-semver").is_none());
assert!(parse_semver("v1.0").is_none());
}
#[test]
fn test_semver_bump() {
let v = SemVer {
major: 1,
minor: 2,
patch: 3,
has_v_prefix: true,
};
assert_eq!(v.bump(BumpType::Patch).to_string(), "v1.2.4");
assert_eq!(v.bump(BumpType::Minor).to_string(), "v1.3.0");
assert_eq!(v.bump(BumpType::Major).to_string(), "v2.0.0");
let v_plain = SemVer {
major: 0,
minor: 1,
patch: 0,
has_v_prefix: false,
};
assert_eq!(v_plain.bump(BumpType::Patch).to_string(), "0.1.1");
assert_eq!(v_plain.bump(BumpType::Minor).to_string(), "0.2.0");
assert_eq!(v_plain.bump(BumpType::Major).to_string(), "1.0.0");
}
}