use anyhow::Result;
use semver::Version;
use crate::version::constraints::{ConstraintSet, VersionConstraint};
#[must_use]
pub fn is_version_constraint(version: &str) -> bool {
if version == "*" {
return true;
}
if version.starts_with('^')
|| version.starts_with('~')
|| version.starts_with('>')
|| version.starts_with('<')
|| version.starts_with('=')
|| version.contains(',')
{
return true;
}
false
}
#[must_use]
pub fn parse_tags_to_versions(tags: Vec<String>) -> Vec<(String, Version)> {
let mut versions = Vec::new();
for tag in tags {
let version_str = tag.strip_prefix('v').unwrap_or(&tag);
if let Ok(version) = Version::parse(version_str) {
versions.push((tag, version));
}
}
versions.sort_by(|a, b| b.1.cmp(&a.1));
versions
}
pub fn find_best_matching_tag(constraint_str: &str, tags: Vec<String>) -> Result<String> {
let constraint = VersionConstraint::parse(constraint_str)?;
let tag_versions = parse_tags_to_versions(tags);
let versions: Vec<Version> = tag_versions.iter().map(|(_, v)| v.clone()).collect();
let mut constraint_set = ConstraintSet::new();
constraint_set.add(constraint)?;
if let Some(best_version) = constraint_set.find_best_match(&versions) {
for (tag_name, version) in tag_versions {
if &version == best_version {
return Ok(tag_name);
}
}
}
Err(anyhow::anyhow!(
"No tag found matching constraint: {}",
constraint_str
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_version_constraint() {
assert!(is_version_constraint("^1.0.0"));
assert!(is_version_constraint("~1.2.0"));
assert!(is_version_constraint(">=1.0.0"));
assert!(is_version_constraint("<2.0.0"));
assert!(is_version_constraint(">=1.0.0, <2.0.0"));
assert!(is_version_constraint("*"));
assert!(!is_version_constraint("v1.0.0"));
assert!(!is_version_constraint("1.0.0"));
assert!(!is_version_constraint("main"));
assert!(!is_version_constraint("develop"));
assert!(!is_version_constraint("abc123def"));
assert!(!is_version_constraint("feature/auth"));
}
#[test]
fn test_parse_tags_to_versions() {
let tags = vec![
"v1.0.0".to_string(),
"1.2.0".to_string(),
"v2.0.0-beta.1".to_string(),
"main".to_string(),
"feature-branch".to_string(),
"v1.5.0".to_string(),
];
let versions = parse_tags_to_versions(tags);
assert_eq!(versions.len(), 4);
assert_eq!(versions[0].0, "v2.0.0-beta.1");
assert_eq!(versions[1].0, "v1.5.0");
assert_eq!(versions[2].0, "1.2.0");
assert_eq!(versions[3].0, "v1.0.0");
}
#[test]
fn test_find_best_matching_tag() {
let tags = vec![
"v1.0.0".to_string(),
"v1.2.0".to_string(),
"v1.5.0".to_string(),
"v2.0.0".to_string(),
"v2.1.0".to_string(),
];
let result = find_best_matching_tag("^1.0.0", tags.clone()).unwrap();
assert_eq!(result, "v1.5.0");
let result = find_best_matching_tag("~1.2.0", tags.clone()).unwrap();
assert_eq!(result, "v1.2.0");
let result = find_best_matching_tag(">=2.0.0", tags.clone()).unwrap();
assert_eq!(result, "v2.1.0");
}
#[test]
fn test_find_best_matching_tag_no_match() {
let tags = vec!["v1.0.0".to_string(), "v2.0.0".to_string()];
let result = find_best_matching_tag("^3.0.0", tags);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("No tag found matching")
);
}
}