use std::cmp::Ordering;
use std::fmt;
use super::PreRelease;
macro_rules! some_or_none_string {
($i:ident) => {
if !$i.is_empty() {
Some($i.to_string())
} else {
None
}
};
}
#[derive(Debug, PartialEq, Clone)]
pub(crate) enum VersionType {
NonProduction,
PreRelease,
Production,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct Semantic {
pub(crate) major: u32,
pub(crate) minor: u32,
pub(crate) patch: u32,
pub(crate) pre_release: Option<PreRelease>,
pub(crate) build_meta_data: Option<String>,
}
impl Ord for Semantic {
fn cmp(&self, other: &Self) -> Ordering {
self.major
.cmp(&other.major)
.then(self.minor.cmp(&other.minor))
.then(self.patch.cmp(&other.patch))
.then(match (&self.pre_release, &other.pre_release) {
(Some(a), Some(b)) => a.cmp(b),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
})
}
}
impl PartialOrd for Semantic {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Semantic {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Semantic {}
impl fmt::Display for Semantic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut version = format!("{}.{}.{}", self.major, self.minor, self.patch);
if let Some(pre) = &self.pre_release {
version = version + "-" + pre.to_string().as_str()
};
if let Some(build) = &self.build_meta_data {
version = version + "+" + build
}
write!(f, "{version}")
}
}
impl Semantic {
pub(crate) fn new(
major: &str,
minor: &str,
patch: &str,
pre_release: &str,
build_meta_data: &str,
) -> Self {
let major: u32 = major.parse().unwrap();
let minor: u32 = minor.parse().unwrap();
let patch: u32 = patch.parse().unwrap();
let pre_release = if pre_release.is_empty() {
None
} else {
Some(PreRelease::new(pre_release))
};
Semantic {
major,
minor,
patch,
pre_release,
build_meta_data: some_or_none_string!(build_meta_data),
}
}
pub(crate) fn increment_pre_release(&mut self) -> &mut Self {
if let Some(mut pre_release) = self.pre_release.clone() {
let new_count = if let Some(mut c) = pre_release.counter {
c += 1;
c
} else {
1
};
pre_release.counter = Some(new_count);
self.pre_release = Some(pre_release);
};
self
}
pub(crate) fn version_type(&self) -> VersionType {
if self.pre_release.is_some() {
return VersionType::PreRelease;
}
if self.major == 0 {
VersionType::NonProduction
} else {
VersionType::Production
}
}
pub(crate) fn is_production_version(&self) -> bool {
self.major != 0
}
}
#[cfg(test)]
mod tests {
use log::LevelFilter;
use rstest::rstest;
use super::*;
fn get_test_logger() {
let mut builder = env_logger::Builder::new();
builder.filter(None, LevelFilter::Debug);
builder.format_timestamp_secs().format_module_path(false);
let _ = builder.try_init();
}
#[test]
fn bump_patch_version_number_by_one() {
let version = Semantic::default();
let mut updated_version = version;
updated_version.patch += 1;
assert_eq!("0.0.1", &updated_version.to_string());
}
#[test]
fn bump_minor_version_number_by_one() {
let mut version = Semantic::default();
version.minor += 1;
version.patch = 0;
assert_eq!("0.1.0", &version.to_string());
}
#[test]
fn bump_major_version_number_by_one() {
let mut version = Semantic::default();
version.major += 1;
version.minor += 0;
version.patch = 0;
assert_eq!("1.0.0", &version.to_string());
}
#[rstest]
#[case::non_prod(0, 7, 9, "", "", "0.7.9")]
#[case::first_alpha(1, 0, 0, "alpha.1", "", "1.0.0-alpha.1")]
#[case::alpha_with_build(1, 0, 0, "alpha.2", "10", "1.0.0-alpha.2+10")]
#[case::beta_with_build(1, 0, 0, "beta.1", "30", "1.0.0-beta.1+30")]
#[case::release_candidate(1, 0, 0, "rc.1", "40", "1.0.0-rc.1+40")]
#[case::first_version(1, 0, 0, "", "", "1.0.0")]
#[case::patched_first_version(1, 0, 1, "", "", "1.0.1")]
#[case::minor_update_first_version(1, 1, 0, "", "", "1.1.0")]
#[case::custom_pre_release(2, 0, 0, "pre.1", "circle.1", "2.0.0-pre.1+circle.1")]
#[case::alphanumeric_build(2, 0, 0, "pre.2", "circle.14", "2.0.0-pre.2+circle.14")]
fn display_value(
#[case] major: u32,
#[case] minor: u32,
#[case] patch: u32,
#[case] pre_release: &str,
#[case] build_meta_data: &str,
#[case] expected: &str,
) {
let pre_release = if !pre_release.is_empty() {
Some(PreRelease::new(pre_release))
} else {
None
};
let build_meta_data = if build_meta_data.is_empty() {
None
} else {
Some(build_meta_data.to_string())
};
let test_version = Semantic {
major,
minor,
patch,
pre_release,
build_meta_data,
};
assert_eq!(expected, test_version.to_string().as_str());
}
#[rstest]
#[case::simple_non_prod(0, 7, 9, "", "", "1.0.0")]
#[case::non_prod_alpha(0, 2, 200, "alpha.1", "", "1.0.0")]
#[case::non_prod_beta_with_build(0, 24, 2, "beta.1", "30", "1.0.0")]
#[case::non_prod_release_candidate(0, 78, 3, "rc.1", "40", "1.0.0")]
#[case::already_first_version(1, 0, 0, "", "", "1.0.0")]
#[case::patched_first_version(1, 0, 1, "", "", "1.0.1")]
#[case::minor_update_first_version(1, 1, 0, "", "", "1.1.0")]
#[case::non_prod_custom_pre_release(0, 23, 1, "pre.1", "circle.1", "1.0.0")]
#[case::post_first_version_alphanumeric_build(
2,
0,
0,
"pre.2",
"circle.14",
"2.0.0-pre.2+circle.14"
)]
fn set_first_production_version_number(
#[case] major: u32,
#[case] minor: u32,
#[case] patch: u32,
#[case] pre_release: &str,
#[case] build_meta_data: &str,
#[case] expected: &str,
) {
get_test_logger();
let pre_release = if !pre_release.is_empty() {
Some(PreRelease::new(pre_release))
} else {
None
};
let build_meta_data = if build_meta_data.is_empty() {
None
} else {
Some(build_meta_data.to_string())
};
let mut test_version = Semantic {
major,
minor,
patch,
pre_release,
build_meta_data,
};
if test_version.major == 0 {
test_version.major = 1;
test_version.minor = 0;
test_version.patch = 0;
test_version.pre_release = None;
test_version.build_meta_data = None;
};
assert_eq!(expected, test_version.to_string().as_str());
}
#[rstest]
#[case::major_gt(
Semantic::new("2", "0", "0", "", ""),
Semantic::new("1", "0", "0", "", "")
)]
#[case::major_gt_despite_minor(
Semantic::new("2", "0", "0", "", ""),
Semantic::new("1", "9", "0", "", "")
)]
#[case::major_gt_despite_patch(
Semantic::new("2", "0", "0", "", ""),
Semantic::new("1", "0", "9", "", "")
)]
fn major_version_precedence(#[case] higher: Semantic, #[case] lower: Semantic) {
assert!(higher > lower, "{higher} should be > {lower}");
assert!(lower < higher, "{lower} should be < {higher}");
}
#[rstest]
#[case::minor_gt(
Semantic::new("1", "1", "0", "", ""),
Semantic::new("1", "0", "0", "", "")
)]
#[case::minor_gt_despite_patch(
Semantic::new("1", "2", "0", "", ""),
Semantic::new("1", "1", "9", "", "")
)]
fn minor_version_precedence(#[case] higher: Semantic, #[case] lower: Semantic) {
assert!(higher > lower, "{higher} should be > {lower}");
assert!(lower < higher, "{lower} should be < {higher}");
}
#[rstest]
#[case::patch_gt(
Semantic::new("1", "0", "1", "", ""),
Semantic::new("1", "0", "0", "", "")
)]
#[case::patch_ordering(
Semantic::new("0", "1", "3", "", ""),
Semantic::new("0", "1", "2", "", "")
)]
fn patch_version_precedence(#[case] higher: Semantic, #[case] lower: Semantic) {
assert!(higher > lower, "{higher} should be > {lower}");
assert!(lower < higher, "{lower} should be < {higher}");
}
#[rstest]
#[case::zeros(
Semantic::new("0", "0", "0", "", ""),
Semantic::new("0", "0", "0", "", "")
)]
#[case::ones(
Semantic::new("1", "1", "1", "", ""),
Semantic::new("1", "1", "1", "", "")
)]
#[case::with_pre(
Semantic::new("1", "0", "0", "alpha.1", ""),
Semantic::new("1", "0", "0", "alpha.1", "")
)]
fn equal_versions(#[case] a: Semantic, #[case] b: Semantic) {
assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal, "{a} should equal {b}");
assert_eq!(a, b, "{a} should == {b}");
}
#[rstest]
#[case::alpha_lt_release(
Semantic::new("1", "0", "0", "alpha", ""),
Semantic::new("1", "0", "0", "", "")
)]
#[case::beta_lt_release(
Semantic::new("1", "0", "0", "beta.1", ""),
Semantic::new("1", "0", "0", "", "")
)]
#[case::rc_lt_release(
Semantic::new("1", "0", "0", "rc.1", ""),
Semantic::new("1", "0", "0", "", "")
)]
#[case::pre_release_non_prod(
Semantic::new("0", "1", "0", "alpha.1", ""),
Semantic::new("0", "1", "0", "", "")
)]
fn pre_release_lower_than_release(#[case] pre: Semantic, #[case] release: Semantic) {
assert!(pre < release, "{pre} should be < {release}");
assert!(release > pre, "{release} should be > {pre}");
assert_ne!(pre, release);
}
#[rstest]
#[case::alpha_lt_beta(
Semantic::new("1", "0", "0", "alpha", ""),
Semantic::new("1", "0", "0", "beta", "")
)]
#[case::alpha_1_lt_alpha_2(
Semantic::new("1", "0", "0", "alpha.1", ""),
Semantic::new("1", "0", "0", "alpha.2", "")
)]
#[case::beta_2_lt_beta_11(
Semantic::new("1", "0", "0", "beta.2", ""),
Semantic::new("1", "0", "0", "beta.11", "")
)]
#[case::beta_lt_rc(
Semantic::new("1", "0", "0", "beta.1", ""),
Semantic::new("1", "0", "0", "rc.1", "")
)]
fn pre_release_ordering(#[case] lower: Semantic, #[case] higher: Semantic) {
assert!(lower < higher, "{lower} should be < {higher}");
assert!(higher > lower, "{higher} should be > {lower}");
}
#[rstest]
#[case::different_builds(
Semantic::new("1", "0", "0", "", "build.1"),
Semantic::new("1", "0", "0", "", "build.2")
)]
#[case::build_vs_no_build(
Semantic::new("1", "0", "0", "", "20130313144700"),
Semantic::new("1", "0", "0", "", "")
)]
#[case::pre_release_different_builds(
Semantic::new("1", "0", "0", "alpha.1", "001"),
Semantic::new("1", "0", "0", "alpha.1", "exp.sha.5114f85")
)]
fn build_metadata_ignored_for_precedence(#[case] a: Semantic, #[case] b: Semantic) {
assert_eq!(
a.cmp(&b),
std::cmp::Ordering::Equal,
"{a} and {b} should have equal precedence"
);
assert_eq!(a, b, "{a} should == {b} (build metadata ignored)");
}
#[test]
fn semver_spec_full_precedence_example() {
let versions = [
Semantic::new("1", "0", "0", "alpha", ""),
Semantic::new("1", "0", "0", "alpha.1", ""),
Semantic::new("1", "0", "0", "alpha.beta", ""),
Semantic::new("1", "0", "0", "beta", ""),
Semantic::new("1", "0", "0", "beta.2", ""),
Semantic::new("1", "0", "0", "beta.11", ""),
Semantic::new("1", "0", "0", "rc.1", ""),
Semantic::new("1", "0", "0", "", ""),
];
for i in 0..versions.len() - 1 {
assert!(
versions[i] < versions[i + 1],
"{} should be < {}",
versions[i],
versions[i + 1]
);
}
}
#[test]
fn sort_produces_semver_order() {
let mut versions = [
Semantic::new("1", "0", "0", "", ""),
Semantic::new("1", "0", "0", "beta.11", ""),
Semantic::new("1", "0", "0", "alpha", ""),
Semantic::new("1", "0", "0", "rc.1", ""),
Semantic::new("1", "0", "0", "beta", ""),
Semantic::new("1", "0", "0", "alpha.1", ""),
Semantic::new("1", "0", "0", "beta.2", ""),
Semantic::new("1", "0", "0", "alpha.beta", ""),
];
versions.sort();
let expected = [
"1.0.0-alpha",
"1.0.0-alpha.1",
"1.0.0-alpha.beta",
"1.0.0-beta",
"1.0.0-beta.2",
"1.0.0-beta.11",
"1.0.0-rc.1",
"1.0.0",
];
let sorted_strings: Vec<String> = versions.iter().map(|v| v.to_string()).collect();
assert_eq!(sorted_strings, expected);
}
#[test]
fn sort_mixed_versions() {
let mut versions = [
Semantic::new("2", "0", "0", "", ""),
Semantic::new("1", "0", "0", "alpha", ""),
Semantic::new("1", "0", "0", "", ""),
Semantic::new("0", "1", "0", "", ""),
Semantic::new("1", "1", "0", "", ""),
Semantic::new("1", "0", "0", "rc.1", ""),
Semantic::new("0", "0", "1", "", ""),
Semantic::new("1", "0", "1", "", ""),
];
versions.sort();
let expected = [
"0.0.1",
"0.1.0",
"1.0.0-alpha",
"1.0.0-rc.1",
"1.0.0",
"1.0.1",
"1.1.0",
"2.0.0",
];
let sorted_strings: Vec<String> = versions.iter().map(|v| v.to_string()).collect();
assert_eq!(sorted_strings, expected);
}
#[test]
fn sort_ignores_build_metadata() {
let mut versions = [
Semantic::new("1", "0", "0", "", "build.2"),
Semantic::new("0", "9", "0", "", ""),
Semantic::new("1", "0", "0", "", "build.1"),
Semantic::new("1", "0", "1", "", ""),
];
versions.sort();
assert_eq!(versions[0].to_string(), "0.9.0");
assert!(versions[1].major == 1 && versions[1].minor == 0 && versions[1].patch == 0);
assert!(versions[2].major == 1 && versions[2].minor == 0 && versions[2].patch == 0);
assert_eq!(versions[3].to_string(), "1.0.1");
}
}