use anyhow::Result;
use semver::Version;
use crate::version::constraints::{ConstraintSet, VersionConstraint};
#[must_use]
pub fn is_version_constraint(version: &str) -> bool {
let (_prefix, version_str) = crate::version::split_prefix_and_version(version);
if version_str == "*" {
return true;
}
if version_str.starts_with('^')
|| version_str.starts_with('~')
|| version_str.starts_with('>')
|| version_str.starts_with('<')
|| version_str.starts_with('=')
|| version_str.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 (_prefix, version_str) = crate::version::split_prefix_and_version(&tag);
let cleaned = version_str.trim_start_matches('v').trim_start_matches('V');
if let Ok(version) = Version::parse(cleaned) {
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_prefix, version_str) = crate::version::split_prefix_and_version(constraint_str);
let filtered_tags: Vec<String> = tags
.into_iter()
.filter(|tag| {
let (tag_prefix, _) = crate::version::split_prefix_and_version(tag);
tag_prefix.as_ref() == constraint_prefix.as_ref()
})
.collect();
if filtered_tags.is_empty() {
return Err(anyhow::anyhow!(
"No tags found with matching prefix for constraint: {constraint_str}"
));
}
let tag_versions = parse_tags_to_versions(filtered_tags);
if tag_versions.is_empty() {
return Err(anyhow::anyhow!(
"No valid semantic version tags found for constraint: {constraint_str}"
));
}
if version_str == "*" {
return Ok(tag_versions[0].0.clone());
}
let constraint = VersionConstraint::parse(version_str)?;
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("latest"));
assert!(!is_version_constraint("latest-prerelease"));
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"));
}
#[test]
fn test_wildcard_matches_highest_version() {
let tags = vec![
"v1.0.0".to_string(),
"v1.2.0".to_string(),
"v2.0.0".to_string(),
"v1.5.0".to_string(),
];
let result = find_best_matching_tag("*", tags).unwrap();
assert_eq!(result, "v2.0.0", "Wildcard should match highest version");
}
#[test]
fn test_prefixed_wildcard_matches_highest_in_namespace() {
let tags = vec![
"agents-v1.0.0".to_string(),
"agents-v1.2.0".to_string(),
"agents-v2.0.0".to_string(),
"snippets-v3.0.0".to_string(), "v5.0.0".to_string(), ];
let result = find_best_matching_tag("agents-*", tags).unwrap();
assert_eq!(
result, "agents-v2.0.0",
"Prefixed wildcard should match highest in that prefix namespace"
);
}
#[test]
fn test_is_version_constraint_with_prefix() {
assert!(is_version_constraint("agents-^v1.0.0"));
assert!(is_version_constraint("snippets-~v2.0.0"));
assert!(is_version_constraint("my-tool->=v1.0.0"));
assert!(is_version_constraint("agents-*"));
assert!(is_version_constraint("snippets-*"));
assert!(is_version_constraint("my-tool-*"));
assert!(!is_version_constraint("agents-v1.0.0"));
assert!(!is_version_constraint("snippets-v2.0.0"));
}
#[test]
fn test_prefixed_wildcards_constraint_detection() {
assert!(is_version_constraint("*"));
assert!(is_version_constraint("agents-*"));
assert!(is_version_constraint("tool123-*"));
assert!(is_version_constraint("my-cool-tool-*"));
assert!(!is_version_constraint("agents-v1.0.0"));
assert!(!is_version_constraint("main"));
assert!(!is_version_constraint("develop"));
}
#[test]
fn test_parse_tags_to_versions_with_prefix() {
let tags = vec![
"agents-v1.0.0".to_string(),
"agents-v2.0.0".to_string(),
"snippets-v1.5.0".to_string(),
"v1.0.0".to_string(),
"main".to_string(),
];
let versions = parse_tags_to_versions(tags);
assert_eq!(versions.len(), 4);
assert!(versions.iter().any(|(tag, _)| tag == "agents-v2.0.0"));
assert!(versions.iter().any(|(tag, _)| tag == "agents-v1.0.0"));
assert!(versions.iter().any(|(tag, _)| tag == "snippets-v1.5.0"));
assert!(versions.iter().any(|(tag, _)| tag == "v1.0.0"));
}
#[test]
fn test_find_best_matching_tag_with_prefix() {
let tags = vec![
"agents-v1.0.0".to_string(),
"agents-v1.2.0".to_string(),
"agents-v2.0.0".to_string(),
"snippets-v1.5.0".to_string(),
"snippets-v2.0.0".to_string(),
"v1.0.0".to_string(),
];
let result = find_best_matching_tag("agents-^v1.0.0", tags.clone()).unwrap();
assert_eq!(result, "agents-v1.2.0");
let result = find_best_matching_tag("snippets-^v1.0.0", tags.clone()).unwrap();
assert_eq!(result, "snippets-v1.5.0");
let result = find_best_matching_tag("^v1.0.0", tags.clone()).unwrap();
assert_eq!(result, "v1.0.0"); }
#[test]
fn test_prefix_isolation_in_matching() {
let tags = vec![
"agents-v1.0.0".to_string(),
"snippets-v2.0.0".to_string(), ];
let result = find_best_matching_tag("agents-^v1.0.0", tags.clone()).unwrap();
assert_eq!(result, "agents-v1.0.0");
}
#[test]
fn test_find_best_matching_tag_no_matching_prefix() {
let tags = vec!["agents-v1.0.0".to_string(), "snippets-v1.0.0".to_string()];
let result = find_best_matching_tag("commands-^v1.0.0", tags);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("No tags found with matching prefix"));
}
#[test]
fn test_parse_prefixed_tags_with_hyphens() {
let tags = vec!["my-cool-agent-v1.0.0".to_string(), "tool-v-v2.0.0".to_string()];
let versions = parse_tags_to_versions(tags);
assert_eq!(versions.len(), 2);
assert!(versions.iter().any(|(tag, ver)| {
tag == "my-cool-agent-v1.0.0" && *ver == Version::parse("1.0.0").unwrap()
}));
assert!(versions.iter().any(|(tag, ver)| {
tag == "tool-v-v2.0.0" && *ver == Version::parse("2.0.0").unwrap()
}));
}
}