use crate::types::TargetLevel;
pub trait SelectableVersion: std::fmt::Display {
fn is_prerelease(&self) -> bool;
fn major(&self) -> u64;
fn minor(&self) -> u64;
fn patch(&self) -> u64;
}
impl SelectableVersion for node_semver::Version {
fn is_prerelease(&self) -> bool {
!self.pre_release.is_empty()
}
fn major(&self) -> u64 {
self.major
}
fn minor(&self) -> u64 {
self.minor
}
fn patch(&self) -> u64 {
self.patch
}
}
impl SelectableVersion for semver::Version {
fn is_prerelease(&self) -> bool {
!self.pre.is_empty()
}
fn major(&self) -> u64 {
self.major
}
fn minor(&self) -> u64 {
self.minor
}
fn patch(&self) -> u64 {
self.patch
}
}
impl SelectableVersion for pep440_rs::Version {
fn is_prerelease(&self) -> bool {
self.any_prerelease()
}
fn major(&self) -> u64 {
self.release().first().copied().unwrap_or(0)
}
fn minor(&self) -> u64 {
self.release().get(1).copied().unwrap_or(0)
}
fn patch(&self) -> u64 {
self.release().get(2).copied().unwrap_or(0)
}
}
#[must_use]
pub fn select_version<V: SelectableVersion>(
current: Option<&V>,
all_versions: &[V],
target: TargetLevel,
latest_for_stable: Option<String>,
unparseable_minor_patch: Option<String>,
) -> Option<String> {
if all_versions.is_empty() {
return latest_for_stable;
}
let current_is_prerelease = current.is_some_and(SelectableVersion::is_prerelease);
let accept = |v: &&V| -> bool {
if !v.is_prerelease() {
return true;
}
current.is_some_and(|cur| {
cur.is_prerelease()
&& v.major() == cur.major()
&& v.minor() == cur.minor()
&& v.patch() == cur.patch()
})
};
match target {
TargetLevel::Latest if current_is_prerelease => all_versions
.iter()
.rev()
.find(accept)
.map(ToString::to_string),
TargetLevel::Latest => latest_for_stable,
TargetLevel::Greatest | TargetLevel::Newest => all_versions.last().map(ToString::to_string),
TargetLevel::Minor => match current {
None => unparseable_minor_patch,
Some(cur) => all_versions
.iter()
.rev()
.find(|v| v.major() == cur.major() && accept(v))
.map(ToString::to_string),
},
TargetLevel::Patch => match current {
None => unparseable_minor_patch,
Some(cur) => all_versions
.iter()
.rev()
.find(|v| v.major() == cur.major() && v.minor() == cur.minor() && accept(v))
.map(ToString::to_string),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
fn vers(specs: &[&str]) -> Vec<semver::Version> {
let mut v: Vec<_> = specs
.iter()
.filter_map(|s| semver::Version::parse(s).ok())
.collect();
v.sort();
v
}
fn parse(s: &str) -> Option<semver::Version> {
semver::Version::parse(s).ok()
}
#[rstest]
#[case::empty_returns_latest_for_stable(
None,
&[],
TargetLevel::Greatest,
Some("1.0.0"),
None,
Some("1.0.0"),
)]
#[case::latest_stable_returns_fallback(
Some("1.0.0"),
&["1.0.0", "1.5.0", "2.0.0"],
TargetLevel::Latest,
Some("2.0.0"),
None,
Some("2.0.0"),
)]
#[case::minor_stays_on_major(
Some("1.0.0"),
&["1.0.0", "1.5.0", "2.0.0"],
TargetLevel::Minor,
None,
None,
Some("1.5.0"),
)]
#[case::patch_stays_on_minor(
Some("1.0.0"),
&["1.0.0", "1.0.5", "1.1.0", "2.0.0"],
TargetLevel::Patch,
None,
None,
Some("1.0.5"),
)]
#[case::greatest_includes_prerelease(
Some("1.0.0"),
&["1.0.0", "2.0.0-rc.1"],
TargetLevel::Greatest,
None,
None,
Some("2.0.0-rc.1"),
)]
#[case::latest_stable_excludes_prerelease_via_fallback(
Some("1.0.0"),
&["1.0.0", "2.0.0-rc.1"],
TargetLevel::Latest,
Some("1.0.0"),
None,
Some("1.0.0"),
)]
#[case::prerelease_tail_same_train(
Some("2.0.0-rc.1"),
&["1.1.0", "2.0.0-rc.1", "2.0.0-rc.2"],
TargetLevel::Latest,
Some("1.1.0"),
None,
Some("2.0.0-rc.2"),
)]
#[case::unparseable_minor_uses_dedicated_fallback(
None,
&["1.0.0", "2.0.0"],
TargetLevel::Minor,
Some("2.0.0"),
None,
None,
)]
#[case::unparseable_patch_uses_dedicated_fallback(
None,
&["1.0.0", "1.0.1", "2.0.0"],
TargetLevel::Patch,
Some("2.0.0"),
None,
None,
)]
#[case::newest_returns_last_candidate(
Some("1.0.0"),
&["1.0.0", "1.5.0", "2.0.0"],
TargetLevel::Newest,
None,
None,
Some("2.0.0"),
)]
fn select_version_cases(
#[case] current_str: Option<&str>,
#[case] version_strs: &[&str],
#[case] target: TargetLevel,
#[case] latest_for_stable: Option<&str>,
#[case] unparseable_minor_patch: Option<&str>,
#[case] expected: Option<&str>,
) {
let cur = current_str.and_then(parse);
let candidates = vers(version_strs);
let selected = select_version(
cur.as_ref(),
&candidates,
target,
latest_for_stable.map(ToOwned::to_owned),
unparseable_minor_patch.map(ToOwned::to_owned),
);
assert_eq!(selected, expected.map(ToOwned::to_owned));
}
#[test]
fn test_selectable_version_trait_accessors() {
let v = semver::Version::parse("3.4.5-beta.1").unwrap();
assert!(v.is_prerelease());
assert_eq!(v.major(), 3);
assert_eq!(v.minor(), 4);
assert_eq!(v.patch(), 5);
let nv = node_semver::Version::parse("6.7.8").unwrap();
assert!(!nv.is_prerelease());
assert_eq!(nv.major(), 6);
assert_eq!(nv.minor(), 7);
assert_eq!(nv.patch(), 8);
let pv: pep440_rs::Version = "9.10.11".parse().unwrap();
assert!(!pv.is_prerelease());
assert_eq!(pv.major(), 9);
assert_eq!(pv.minor(), 10);
assert_eq!(pv.patch(), 11);
let pre: pep440_rs::Version = "2.0a1".parse().unwrap();
assert!(pre.is_prerelease());
assert_eq!(pre.major(), 2);
assert_eq!(pre.minor(), 0);
let dev: pep440_rs::Version = "1.0.dev0".parse().unwrap();
assert!(dev.is_prerelease());
let short: pep440_rs::Version = "5".parse().unwrap();
assert_eq!(short.major(), 5);
assert_eq!(short.minor(), 0);
assert_eq!(short.patch(), 0);
}
}