use anyhow::Result;
use semver::{Version, VersionReq};
pub fn parse_constraint(spec: &str) -> Result<VersionReq> {
let spec = spec.trim();
if spec == "*" || spec.is_empty() {
return Ok(VersionReq::STAR);
}
if spec.contains('|') {
let parts: Vec<&str> = if spec.contains("||") {
spec.split("||").collect()
} else {
spec.split('|').collect()
};
let mut best_constraint = None;
let mut best_score = 0;
for part in &parts {
let trimmed = part.trim();
if !trimmed.is_empty() {
if let Ok(constraint) = parse_simple_constraint(trimmed) {
let score = score_constraint_permissiveness(trimmed);
if score > best_score {
best_score = score;
best_constraint = Some(constraint);
}
}
}
}
if let Some(constraint) = best_constraint {
return Ok(constraint);
}
for part in &parts {
let trimmed = part.trim();
if !trimmed.is_empty() {
if let Ok(constraint) = parse_simple_constraint(trimmed) {
return Ok(constraint);
}
}
}
}
parse_simple_constraint(spec)
}
fn score_constraint_permissiveness(constraint: &str) -> i32 {
if constraint.starts_with(">=") && !constraint.contains('<') {
return 100; }
if constraint.starts_with('^') {
if let Some(version_part) = constraint.strip_prefix('^') {
if let Ok(major) = version_part.split('.').next().unwrap_or("0").parse::<u32>() {
return 50 + major as i32; }
}
return 50; }
if constraint.starts_with('~') {
return 30; }
if constraint.starts_with('=') {
return 10; }
if constraint.starts_with(">=") && constraint.contains('<') {
return 40; }
20 }
fn parse_simple_constraint(spec: &str) -> Result<VersionReq> {
let spec = spec.trim();
if spec.starts_with("dev-") {
return Ok(VersionReq::parse(">=999.0.0-dev")?);
}
if spec.starts_with('^')
|| spec.starts_with('~')
|| spec.starts_with(">=")
|| spec.starts_with("<=")
|| spec.starts_with('>')
|| spec.starts_with('<')
{
let normalized = normalize_version_in_constraint(spec)?;
return Ok(VersionReq::parse(&normalized)?);
}
if spec.contains(" - ") {
let parts: Vec<&str> = spec.split(" - ").collect();
if parts.len() == 2 {
let start = normalize_semver_string(parts[0].trim())?;
let end = normalize_semver_string(parts[1].trim())?;
return Ok(VersionReq::parse(&format!(">={start}, <={end}"))?);
}
}
if spec.contains(',') {
return Ok(VersionReq::parse(spec)?);
}
let normalized = normalize_semver_string(spec)?;
if Version::parse(&normalized).is_ok() {
return Ok(VersionReq::parse(&format!("={normalized}"))?);
}
Ok(VersionReq::parse(&normalized).unwrap_or(VersionReq::STAR))
}
fn normalize_version_in_constraint(constraint: &str) -> Result<String> {
if let Some(version_part) = constraint.strip_prefix('^') {
let normalized = normalize_semver_string(version_part)?;
Ok(format!("^{normalized}"))
} else if let Some(version_part) = constraint.strip_prefix('~') {
let normalized = normalize_semver_string(version_part)?;
Ok(format!("~{normalized}"))
} else if let Some(version_part) = constraint.strip_prefix(">=") {
let normalized = normalize_semver_string(version_part.trim())?;
Ok(format!(">={normalized}"))
} else if let Some(version_part) = constraint.strip_prefix("<=") {
let normalized = normalize_semver_string(version_part.trim())?;
Ok(format!("<={normalized}"))
} else if let Some(version_part) = constraint.strip_prefix('>') {
let normalized = normalize_semver_string(version_part.trim())?;
Ok(format!(">{normalized}"))
} else if let Some(version_part) = constraint.strip_prefix('<') {
let normalized = normalize_semver_string(version_part.trim())?;
Ok(format!("<{normalized}"))
} else {
Ok(constraint.to_string())
}
}
fn normalize_semver_string(s: &str) -> Result<String> {
let s = s.trim().strip_prefix('v').unwrap_or(s.trim());
let (version_part, stability_suffix) = if let Some(idx) = s.find('-') {
let (v, suffix) = s.split_at(idx);
(v, Some(suffix))
} else {
(s, None)
};
let parts: Vec<&str> = version_part.split('.').collect();
if parts.is_empty() {
return Err(anyhow::anyhow!("Invalid version: empty"));
}
let major = parts.first().unwrap_or(&"0");
let minor = parts.get(1).unwrap_or(&"0");
let patch = parts.get(2).unwrap_or(&"0");
let clean_part = |part: &str| -> Result<String> {
if part.chars().all(char::is_numeric) && !part.is_empty() {
Ok(part.parse::<u32>().unwrap_or(0).to_string())
} else if part == "*" {
Ok("0".to_string())
} else {
Err(anyhow::anyhow!("Invalid version part: {}", part))
}
};
let major_clean = clean_part(major)?;
let minor_clean = clean_part(minor)?;
let patch_clean = clean_part(patch)?;
let normalized = format!("{major_clean}.{minor_clean}.{patch_clean}");
if let Some(suffix) = stability_suffix {
Ok(format!("{normalized}{suffix}"))
} else {
Ok(normalized)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_constraint() {
assert!(parse_constraint("^1.2.3").is_ok());
assert!(parse_constraint("~1.2").is_ok());
assert!(parse_constraint(">=1.0.0").is_ok());
assert!(parse_constraint("*").is_ok());
assert!(parse_constraint("dev-master").is_ok());
}
#[test]
fn test_or_constraints() {
assert!(parse_constraint("^2|^3").is_ok());
assert!(parse_constraint("^1.0||^2.0").is_ok());
}
#[test]
fn test_normalize_semver_string() {
assert_eq!(normalize_semver_string("1.2.3").unwrap(), "1.2.3");
assert_eq!(normalize_semver_string("v1.2.3").unwrap(), "1.2.3");
assert_eq!(normalize_semver_string("1.2").unwrap(), "1.2.0");
assert_eq!(normalize_semver_string("1").unwrap(), "1.0.0");
}
}