use crate::module::traits::ModuleError;
pub fn parse_semver_triple(version: &str) -> Option<(u64, u64, u64)> {
let version = version.trim().trim_start_matches('v');
let core = version.split(['-', '+']).next().unwrap_or(version);
let mut parts = core.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts.next()?.parse().ok()?;
if parts.next().is_some() {
return None;
}
Some((major, minor, patch))
}
pub fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
let va = parse_semver_triple(a).unwrap_or((0, 0, 0));
let vb = parse_semver_triple(b).unwrap_or((0, 0, 0));
va.cmp(&vb)
}
pub fn matches_version_constraint(version: &str, constraint: &str) -> bool {
let constraint = constraint.trim();
if constraint.is_empty() || constraint == "*" {
return true;
}
let Some(v) = parse_semver_triple(version) else {
return false;
};
let constraint = constraint.trim_start_matches('v');
let segments: Vec<&str> = constraint.split('.').collect();
if segments.is_empty() {
return false;
}
for (i, seg) in segments.iter().enumerate() {
if *seg == "*" {
return true;
}
let v_part = match i {
0 => v.0,
1 => v.1,
2 => v.2,
_ => return false,
};
let Ok(c_part) = seg.parse::<u64>() else {
return false;
};
if v_part != c_part {
return false;
}
}
true
}
pub fn select_highest_matching_version(
versions: &[String],
constraint: &str,
) -> Result<String, ModuleError> {
let constraint = constraint.trim();
if constraint.is_empty() || constraint == "*" {
return Err(ModuleError::OperationError(
"select_highest_matching_version requires a non-wildcard constraint".to_string(),
));
}
let mut matches: Vec<&String> = versions
.iter()
.filter(|v| matches_version_constraint(v, constraint))
.collect();
if matches.is_empty() {
return Err(ModuleError::OperationError(format!(
"No release version matches constraint '{constraint}' (available: {versions:?})"
)));
}
matches.sort_by(|a, b| compare_versions(a, b));
Ok(matches.pop().unwrap().clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_version_match() {
assert!(matches_version_constraint("0.1.2", "0.1.2"));
assert!(!matches_version_constraint("0.1.3", "0.1.2"));
}
#[test]
fn patch_wildcard() {
assert!(matches_version_constraint("0.1.0", "0.1.*"));
assert!(matches_version_constraint("0.1.99", "0.1.*"));
assert!(!matches_version_constraint("0.2.0", "0.1.*"));
}
#[test]
fn minor_wildcard() {
assert!(matches_version_constraint("0.3.1", "0.*"));
assert!(matches_version_constraint("0.0.9", "0.*"));
assert!(!matches_version_constraint("1.0.0", "0.*"));
}
#[test]
fn star_matches_any() {
assert!(matches_version_constraint("9.9.9", "*"));
assert!(matches_version_constraint("0.0.1", ""));
}
#[test]
fn leading_v_stripped() {
assert!(matches_version_constraint("v0.1.2", "0.1.*"));
}
#[test]
fn select_highest_matching() {
let versions = vec![
"0.1.0".to_string(),
"0.1.5".to_string(),
"0.2.0".to_string(),
];
assert_eq!(
select_highest_matching_version(&versions, "0.1.*").unwrap(),
"0.1.5"
);
}
#[test]
fn parse_semver_triple_rejects_extra_segments() {
assert!(parse_semver_triple("1.2.3.4").is_none());
}
#[test]
fn parse_semver_triple_strips_prerelease() {
assert_eq!(parse_semver_triple("1.2.3-rc1"), Some((1, 2, 3)));
}
#[test]
fn compare_versions_orders_semver() {
assert_eq!(
compare_versions("0.1.10", "0.1.2"),
std::cmp::Ordering::Greater
);
}
#[test]
fn select_highest_matching_no_match_errors() {
let versions = vec!["0.2.0".to_string()];
assert!(select_highest_matching_version(&versions, "0.1.*").is_err());
}
}