use semver::{Prerelease, Version};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum Bump {
Patch,
Minor,
Major,
}
impl Bump {
pub(crate) fn as_str(self) -> &'static str {
match self {
Bump::Patch => "patch",
Bump::Minor => "minor",
Bump::Major => "major",
}
}
}
pub(crate) fn next_version(current: &Version, bump: Bump) -> Version {
let pre = !current.pre.is_empty();
match bump {
Bump::Major if pre && current.minor == 0 && current.patch == 0 => {
Version::new(current.major, 0, 0)
}
Bump::Major => Version::new(checked_inc(current.major), 0, 0),
Bump::Minor if pre && current.patch == 0 => Version::new(current.major, current.minor, 0),
Bump::Minor => Version::new(current.major, checked_inc(current.minor), 0),
Bump::Patch if pre => Version::new(current.major, current.minor, current.patch),
Bump::Patch => Version::new(current.major, current.minor, checked_inc(current.patch)),
}
}
fn checked_inc(number: u64) -> u64 {
number.checked_add(1).expect("version number overflow")
}
pub(crate) fn pre_counter(current: &Version, tag: &str) -> u64 {
current
.pre
.as_str()
.strip_prefix(&format!("{tag}."))
.and_then(|rest| rest.parse::<u64>().ok())
.map_or(0, checked_inc)
}
pub(crate) fn next_pre_version(current: &Version, bump: Bump, tag: &str) -> Version {
next_pre_version_with(current, bump, tag, pre_counter(current, tag))
}
pub(crate) fn next_pre_version_with(
current: &Version,
bump: Bump,
tag: &str,
counter: u64,
) -> Version {
let mut version = next_version(current, bump);
version.pre =
Prerelease::new(&format!("{tag}.{counter}")).expect("a validated tag stays valid");
version
}
#[cfg(test)]
mod tests {
use super::*;
fn next(current: &str, bump: Bump) -> String {
next_version(¤t.parse().unwrap(), bump).to_string()
}
fn next_pre(current: &str, bump: Bump, tag: &str) -> String {
next_pre_version(¤t.parse().unwrap(), bump, tag).to_string()
}
#[test]
fn bump_orders_patch_below_minor_below_major() {
assert!(Bump::Patch < Bump::Minor);
assert!(Bump::Minor < Bump::Major);
}
#[test]
fn max_picks_the_highest_bump() {
assert_eq!(
[Bump::Patch, Bump::Major, Bump::Minor].iter().max(),
Some(&Bump::Major)
);
assert_eq!([Bump::Patch, Bump::Minor].iter().max(), Some(&Bump::Minor));
assert_eq!([Bump::Patch].iter().max(), Some(&Bump::Patch));
assert_eq!([Bump::Patch; 0].iter().max(), None);
}
#[test]
fn increments_literally() {
assert_eq!(next("1.2.3", Bump::Major), "2.0.0");
assert_eq!(next("1.2.3", Bump::Minor), "1.3.0");
assert_eq!(next("1.2.3", Bump::Patch), "1.2.4");
}
#[test]
fn increments_literally_on_zero_major() {
assert_eq!(next("0.5.2", Bump::Major), "1.0.0");
assert_eq!(next("0.5.2", Bump::Minor), "0.6.0");
assert_eq!(next("0.5.2", Bump::Patch), "0.5.3");
assert_eq!(next("0.0.1", Bump::Major), "1.0.0");
}
#[test]
fn graduates_a_pre_release_that_satisfies_the_bump() {
assert_eq!(next("2.0.0-beta.1", Bump::Major), "2.0.0");
assert_eq!(next("1.2.0-rc.1", Bump::Minor), "1.2.0");
assert_eq!(next("1.2.3-rc.1", Bump::Patch), "1.2.3");
}
#[test]
fn increments_past_a_pre_release_that_does_not_satisfy_the_bump() {
assert_eq!(next("2.1.0-beta.1", Bump::Major), "3.0.0");
assert_eq!(next("2.0.1-beta.1", Bump::Major), "3.0.0");
assert_eq!(next("1.2.3-rc.1", Bump::Minor), "1.3.0");
}
#[test]
fn clears_build_metadata() {
assert_eq!(next("1.2.3+abc", Bump::Major), "2.0.0");
assert_eq!(next("1.2.3+abc", Bump::Minor), "1.3.0");
assert_eq!(next("1.2.3+abc", Bump::Patch), "1.2.4");
assert_eq!(next("1.2.3-rc.1+abc", Bump::Patch), "1.2.3");
}
#[test]
fn pre_version_starts_at_zero() {
assert_eq!(next_pre("1.0.0", Bump::Minor, "beta"), "1.1.0-beta.0");
assert_eq!(next_pre("1.0.0", Bump::Major, "beta"), "2.0.0-beta.0");
assert_eq!(next_pre("1.0.0", Bump::Patch, "beta"), "1.0.1-beta.0");
}
#[test]
fn pre_version_increments_the_counter() {
assert_eq!(
next_pre("1.1.0-beta.0", Bump::Patch, "beta"),
"1.1.0-beta.1"
);
}
#[test]
fn pre_version_keeps_counting_across_base_bumps() {
assert_eq!(
next_pre("1.1.0-beta.1", Bump::Major, "beta"),
"2.0.0-beta.2"
);
assert_eq!(
next_pre("1.0.1-beta.0", Bump::Minor, "beta"),
"1.1.0-beta.1"
);
}
#[test]
fn pre_version_handles_a_dotted_tag() {
assert_eq!(
next_pre("1.1.0-beta.2.0", Bump::Patch, "beta.2"),
"1.1.0-beta.2.1"
);
}
#[test]
fn pre_version_restarts_on_a_tag_switch() {
assert_eq!(
next_pre("1.1.0-alpha.3", Bump::Patch, "beta"),
"1.1.0-beta.0"
);
}
#[test]
fn pre_version_restarts_on_a_non_numeric_counter() {
assert_eq!(
next_pre("1.0.0-alpha.beta", Bump::Patch, "alpha"),
"1.0.0-alpha.0"
);
}
#[test]
fn pre_version_with_a_numeric_tag() {
assert_eq!(next_pre("1.0.0", Bump::Patch, "1"), "1.0.1-1.0");
assert_eq!(next_pre("1.0.1-1.0", Bump::Patch, "1"), "1.0.1-1.1");
}
#[test]
fn pre_version_clears_build_metadata() {
assert_eq!(next_pre("1.2.3+abc", Bump::Minor, "beta"), "1.3.0-beta.0");
}
fn counter(current: &str, tag: &str) -> u64 {
pre_counter(¤t.parse().unwrap(), tag)
}
#[test]
fn pre_counter_continues_a_matching_tag() {
assert_eq!(counter("1.1.0-beta.0", "beta"), 1);
assert_eq!(counter("1.1.0-beta.2.3", "beta.2"), 4);
}
#[test]
fn pre_counter_restarts_otherwise() {
assert_eq!(counter("1.0.0", "beta"), 0);
assert_eq!(counter("1.1.0-alpha.3", "beta"), 0);
assert_eq!(counter("1.0.0-alpha.beta", "alpha"), 0);
}
#[test]
#[should_panic(expected = "version number overflow")]
fn panics_on_a_version_component_overflow() {
next("18446744073709551615.0.0", Bump::Major);
}
#[test]
#[should_panic(expected = "version number overflow")]
fn pre_counter_panics_on_a_counter_overflow() {
counter("1.0.0-beta.18446744073709551615", "beta");
}
#[test]
fn pre_version_with_a_given_counter() {
assert_eq!(
next_pre_version_with(&"1.1.0-beta.0".parse().unwrap(), Bump::Patch, "beta", 5)
.to_string(),
"1.1.0-beta.5"
);
}
}