use anyhow::Result;
use semver::{Version, VersionReq};
use std::collections::HashMap;
use std::fmt;
use crate::core::CcpmError;
#[derive(Debug, Clone)]
pub enum VersionConstraint {
Exact(Version),
Requirement(VersionReq),
Latest,
LatestPrerelease,
GitRef(String),
}
impl VersionConstraint {
pub fn parse(constraint: &str) -> Result<Self> {
let trimmed = constraint.trim();
if trimmed == "latest" || trimmed == "*" {
return Ok(Self::Latest);
}
if trimmed == "latest-prerelease" {
return Ok(Self::LatestPrerelease);
}
let version_str = trimmed.strip_prefix('v').unwrap_or(trimmed);
if let Ok(version) = Version::parse(version_str) {
if !trimmed.starts_with('^')
&& !trimmed.starts_with('~')
&& !trimmed.starts_with('>')
&& !trimmed.starts_with('<')
&& !trimmed.starts_with('=')
{
return Ok(Self::Exact(version));
}
}
if let Ok(req) = VersionReq::parse(trimmed) {
return Ok(Self::Requirement(req));
}
Ok(Self::GitRef(trimmed.to_string()))
}
#[must_use]
pub fn matches(&self, version: &Version) -> bool {
match self {
Self::Exact(v) => v == version,
Self::Requirement(req) => req.matches(version),
Self::Latest => version.pre.is_empty(), Self::LatestPrerelease => true, Self::GitRef(_) => false, }
}
#[must_use]
pub fn matches_ref(&self, git_ref: &str) -> bool {
match self {
Self::GitRef(ref_name) => ref_name == git_ref,
_ => false,
}
}
#[must_use]
pub fn to_version_req(&self) -> Option<VersionReq> {
if matches!(
self,
Self::Exact(_) | Self::Requirement(_) | Self::Latest | Self::LatestPrerelease
) {
match self {
Self::Exact(v) => {
VersionReq::parse(&format!("={v}")).ok()
}
Self::Requirement(req) => Some(req.clone()),
Self::Latest | Self::LatestPrerelease => {
VersionReq::parse("*").ok()
}
_ => unreachable!(),
}
} else {
None
}
}
#[must_use]
pub fn allows_prerelease(&self) -> bool {
matches!(self, Self::LatestPrerelease | Self::GitRef(_))
}
}
impl fmt::Display for VersionConstraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Exact(v) => write!(f, "{v}"),
Self::Requirement(req) => write!(f, "{req}"),
Self::Latest => write!(f, "latest"),
Self::LatestPrerelease => write!(f, "latest-prerelease"),
Self::GitRef(ref_name) => write!(f, "{ref_name}"),
}
}
}
#[derive(Debug, Clone)]
pub struct ConstraintSet {
constraints: Vec<VersionConstraint>,
}
impl Default for ConstraintSet {
fn default() -> Self {
Self::new()
}
}
impl ConstraintSet {
#[must_use]
pub fn new() -> Self {
Self {
constraints: Vec::new(),
}
}
pub fn add(&mut self, constraint: VersionConstraint) -> Result<()> {
if self.has_conflict(&constraint) {
return Err(CcpmError::Other {
message: format!("Constraint {constraint} conflicts with existing constraints"),
}
.into());
}
self.constraints.push(constraint);
Ok(())
}
#[must_use]
pub fn satisfies(&self, version: &Version) -> bool {
self.constraints.iter().all(|c| c.matches(version))
}
#[must_use]
pub fn find_best_match<'a>(&self, versions: &'a [Version]) -> Option<&'a Version> {
let mut candidates: Vec<&Version> = versions.iter().filter(|v| self.satisfies(v)).collect();
candidates.sort_by(|a, b| b.cmp(a));
if !self.allows_prerelease() {
candidates.retain(|v| v.pre.is_empty());
}
candidates.first().copied()
}
#[must_use]
pub fn allows_prerelease(&self) -> bool {
self.constraints
.iter()
.any(VersionConstraint::allows_prerelease)
}
fn has_conflict(&self, new_constraint: &VersionConstraint) -> bool {
for existing in &self.constraints {
match (existing, new_constraint) {
(VersionConstraint::Exact(v1), VersionConstraint::Exact(v2)) => {
if v1 != v2 {
return true;
}
}
(VersionConstraint::GitRef(r1), VersionConstraint::GitRef(r2)) => {
if r1 != r2 {
return true;
}
}
_ => {
}
}
}
false
}
}
pub struct ConstraintResolver {
constraints: HashMap<String, ConstraintSet>,
}
impl Default for ConstraintResolver {
fn default() -> Self {
Self::new()
}
}
impl ConstraintResolver {
#[must_use]
pub fn new() -> Self {
Self {
constraints: HashMap::new(),
}
}
pub fn add_constraint(&mut self, dependency: &str, constraint: &str) -> Result<()> {
let parsed = VersionConstraint::parse(constraint)?;
self.constraints
.entry(dependency.to_string())
.or_default()
.add(parsed)?;
Ok(())
}
pub fn resolve(
&self,
available_versions: &HashMap<String, Vec<Version>>,
) -> Result<HashMap<String, Version>> {
let mut resolved = HashMap::new();
for (dep, constraint_set) in &self.constraints {
let versions = available_versions
.get(dep)
.ok_or_else(|| CcpmError::Other {
message: format!("No versions available for dependency: {dep}"),
})?;
let best_match =
constraint_set
.find_best_match(versions)
.ok_or_else(|| CcpmError::Other {
message: format!("No version satisfies constraints for dependency: {dep}"),
})?;
resolved.insert(dep.clone(), best_match.clone());
}
Ok(resolved)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_constraint_parse() {
let constraint = VersionConstraint::parse("1.0.0").unwrap();
assert!(matches!(constraint, VersionConstraint::Exact(_)));
let constraint = VersionConstraint::parse("v1.0.0").unwrap();
assert!(matches!(constraint, VersionConstraint::Exact(_)));
let constraint = VersionConstraint::parse("^1.0.0").unwrap();
assert!(matches!(constraint, VersionConstraint::Requirement(_)));
let constraint = VersionConstraint::parse("~1.2.0").unwrap();
assert!(matches!(constraint, VersionConstraint::Requirement(_)));
let constraint = VersionConstraint::parse(">=1.0.0, <2.0.0").unwrap();
assert!(matches!(constraint, VersionConstraint::Requirement(_)));
let constraint = VersionConstraint::parse("latest").unwrap();
assert!(matches!(constraint, VersionConstraint::Latest));
let constraint = VersionConstraint::parse("main").unwrap();
assert!(matches!(constraint, VersionConstraint::GitRef(_)));
}
#[test]
fn test_constraint_matching() {
let v100 = Version::parse("1.0.0").unwrap();
let v110 = Version::parse("1.1.0").unwrap();
let v200 = Version::parse("2.0.0").unwrap();
let exact = VersionConstraint::Exact(v100.clone());
assert!(exact.matches(&v100));
assert!(!exact.matches(&v110));
let caret = VersionConstraint::parse("^1.0.0").unwrap();
assert!(caret.matches(&v100));
assert!(caret.matches(&v110));
assert!(!caret.matches(&v200));
let latest = VersionConstraint::Latest;
assert!(latest.matches(&v100));
assert!(latest.matches(&v200));
}
#[test]
fn test_constraint_set() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::parse(">=1.0.0").unwrap())
.unwrap();
set.add(VersionConstraint::parse("<2.0.0").unwrap())
.unwrap();
let v090 = Version::parse("0.9.0").unwrap();
let v100 = Version::parse("1.0.0").unwrap();
let v150 = Version::parse("1.5.0").unwrap();
let v200 = Version::parse("2.0.0").unwrap();
assert!(!set.satisfies(&v090));
assert!(set.satisfies(&v100));
assert!(set.satisfies(&v150));
assert!(!set.satisfies(&v200));
}
#[test]
fn test_find_best_match() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::parse("^1.0.0").unwrap())
.unwrap();
let versions = vec![
Version::parse("0.9.0").unwrap(),
Version::parse("1.0.0").unwrap(),
Version::parse("1.2.0").unwrap(),
Version::parse("1.5.0").unwrap(),
Version::parse("2.0.0").unwrap(),
];
let best = set.find_best_match(&versions).unwrap();
assert_eq!(best, &Version::parse("1.5.0").unwrap());
}
#[test]
fn test_constraint_conflicts() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::Exact(Version::parse("1.0.0").unwrap()))
.unwrap();
let result = set.add(VersionConstraint::Exact(Version::parse("2.0.0").unwrap()));
assert!(result.is_err());
let result = set.add(VersionConstraint::Exact(Version::parse("1.0.0").unwrap()));
assert!(result.is_ok());
}
#[test]
fn test_constraint_resolver() {
let mut resolver = ConstraintResolver::new();
resolver.add_constraint("dep1", "^1.0.0").unwrap();
resolver.add_constraint("dep2", "~2.1.0").unwrap();
let mut available = HashMap::new();
available.insert(
"dep1".to_string(),
vec![
Version::parse("0.9.0").unwrap(),
Version::parse("1.0.0").unwrap(),
Version::parse("1.5.0").unwrap(),
Version::parse("2.0.0").unwrap(),
],
);
available.insert(
"dep2".to_string(),
vec![
Version::parse("2.0.0").unwrap(),
Version::parse("2.1.0").unwrap(),
Version::parse("2.1.5").unwrap(),
Version::parse("2.2.0").unwrap(),
],
);
let resolved = resolver.resolve(&available).unwrap();
assert_eq!(
resolved.get("dep1"),
Some(&Version::parse("1.5.0").unwrap())
);
assert_eq!(
resolved.get("dep2"),
Some(&Version::parse("2.1.5").unwrap())
);
}
#[test]
fn test_allows_prerelease() {
assert!(!VersionConstraint::Latest.allows_prerelease());
assert!(VersionConstraint::LatestPrerelease.allows_prerelease());
assert!(VersionConstraint::GitRef("main".to_string()).allows_prerelease());
assert!(!VersionConstraint::Exact(Version::parse("1.0.0").unwrap()).allows_prerelease());
}
#[test]
fn test_version_constraint_parse_edge_cases() {
let constraint = VersionConstraint::parse("latest-prerelease").unwrap();
assert!(matches!(constraint, VersionConstraint::LatestPrerelease));
let constraint = VersionConstraint::parse("*").unwrap();
assert!(matches!(constraint, VersionConstraint::Latest));
let constraint = VersionConstraint::parse(">=1.0.0").unwrap();
assert!(matches!(constraint, VersionConstraint::Requirement(_)));
let constraint = VersionConstraint::parse("<2.0.0").unwrap();
assert!(matches!(constraint, VersionConstraint::Requirement(_)));
let constraint = VersionConstraint::parse("=1.0.0").unwrap();
assert!(matches!(constraint, VersionConstraint::Requirement(_)));
let constraint = VersionConstraint::parse("feature/new-feature").unwrap();
assert!(matches!(constraint, VersionConstraint::GitRef(_)));
let constraint = VersionConstraint::parse("abc123def456").unwrap();
assert!(matches!(constraint, VersionConstraint::GitRef(_)));
}
#[test]
fn test_version_constraint_display() {
let exact = VersionConstraint::Exact(Version::parse("1.0.0").unwrap());
assert_eq!(format!("{exact}"), "1.0.0");
let req = VersionConstraint::parse("^1.0.0").unwrap();
assert_eq!(format!("{req}"), "^1.0.0");
let latest = VersionConstraint::Latest;
assert_eq!(format!("{latest}"), "latest");
let latest_pre = VersionConstraint::LatestPrerelease;
assert_eq!(format!("{latest_pre}"), "latest-prerelease");
let git_ref = VersionConstraint::GitRef("main".to_string());
assert_eq!(format!("{git_ref}"), "main");
}
#[test]
fn test_version_constraint_matches_ref() {
let git_ref = VersionConstraint::GitRef("main".to_string());
assert!(git_ref.matches_ref("main"));
assert!(!git_ref.matches_ref("develop"));
let exact = VersionConstraint::Exact(Version::parse("1.0.0").unwrap());
assert!(!exact.matches_ref("v1.0.0"));
let latest = VersionConstraint::Latest;
assert!(!latest.matches_ref("latest"));
}
#[test]
fn test_version_constraint_to_version_req() {
let exact = VersionConstraint::Exact(Version::parse("1.0.0").unwrap());
let req = exact.to_version_req().unwrap();
assert!(req.matches(&Version::parse("1.0.0").unwrap()));
let caret = VersionConstraint::parse("^1.0.0").unwrap();
let req = caret.to_version_req().unwrap();
assert!(req.matches(&Version::parse("1.0.0").unwrap()));
let latest = VersionConstraint::Latest;
let req = latest.to_version_req().unwrap();
assert!(req.matches(&Version::parse("1.0.0").unwrap()));
let git_ref = VersionConstraint::GitRef("main".to_string());
assert!(git_ref.to_version_req().is_none());
}
#[test]
fn test_constraint_set_with_prereleases() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::LatestPrerelease).unwrap();
let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
let v100 = Version::parse("1.0.0").unwrap();
assert!(set.allows_prerelease());
let versions = vec![v100_pre.clone(), v100.clone()];
let best = set.find_best_match(&versions).unwrap();
assert_eq!(best, &v100); }
#[test]
fn test_constraint_set_no_matches() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::parse(">=2.0.0").unwrap())
.unwrap();
let versions = vec![
Version::parse("1.0.0").unwrap(),
Version::parse("1.5.0").unwrap(),
];
let best = set.find_best_match(&versions);
assert!(best.is_none());
}
#[test]
fn test_constraint_resolver_missing_dependency() {
let mut resolver = ConstraintResolver::new();
resolver.add_constraint("dep1", "^1.0.0").unwrap();
let available = HashMap::new();
let result = resolver.resolve(&available);
assert!(result.is_err());
}
#[test]
fn test_constraint_resolver_no_satisfying_version() {
let mut resolver = ConstraintResolver::new();
resolver.add_constraint("dep1", "^2.0.0").unwrap();
let mut available = HashMap::new();
available.insert(
"dep1".to_string(),
vec![Version::parse("1.0.0").unwrap()], );
let result = resolver.resolve(&available);
assert!(result.is_err());
}
#[test]
fn test_constraint_set_git_ref_conflicts() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::GitRef("main".to_string()))
.unwrap();
let result = set.add(VersionConstraint::GitRef("develop".to_string()));
assert!(result.is_err());
let result = set.add(VersionConstraint::GitRef("main".to_string()));
assert!(result.is_ok());
}
#[test]
fn test_latest_constraint_with_prereleases() {
let latest = VersionConstraint::Latest;
let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
let v100 = Version::parse("1.0.0").unwrap();
assert!(latest.matches(&v100));
assert!(!latest.matches(&v100_pre));
}
#[test]
fn test_latest_prerelease_constraint() {
let latest_pre = VersionConstraint::LatestPrerelease;
let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
let v100 = Version::parse("1.0.0").unwrap();
assert!(latest_pre.matches(&v100));
assert!(latest_pre.matches(&v100_pre));
}
#[test]
fn test_requirement_constraint_allows_prerelease() {
let req = VersionConstraint::parse("^1.0.0").unwrap();
assert!(!req.allows_prerelease());
let exact = VersionConstraint::Exact(Version::parse("1.0.0").unwrap());
assert!(!exact.allows_prerelease());
}
#[test]
fn test_constraint_set_prerelease_filtering() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::parse("^1.0.0").unwrap())
.unwrap();
let versions = vec![
Version::parse("1.0.0-alpha.1").unwrap(),
Version::parse("1.0.0").unwrap(),
Version::parse("1.1.0-beta.1").unwrap(),
Version::parse("1.1.0").unwrap(),
];
let best = set.find_best_match(&versions).unwrap();
assert_eq!(best, &Version::parse("1.1.0").unwrap()); }
#[test]
fn test_parse_with_whitespace() {
let constraint = VersionConstraint::parse(" 1.0.0 ").unwrap();
assert!(matches!(constraint, VersionConstraint::Exact(_)));
let constraint = VersionConstraint::parse(" latest ").unwrap();
assert!(matches!(constraint, VersionConstraint::Latest));
let constraint = VersionConstraint::parse(" ^1.0.0 ").unwrap();
assert!(matches!(constraint, VersionConstraint::Requirement(_)));
}
#[test]
fn test_constraint_resolver_add_constraint_error() {
let mut resolver = ConstraintResolver::new();
resolver.add_constraint("dep1", "1.0.0").unwrap();
let result = resolver.add_constraint("dep1", "2.0.0");
assert!(result.is_err());
}
#[test]
fn test_constraint_set_no_conflict_different_types() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::parse("^1.0.0").unwrap())
.unwrap();
set.add(VersionConstraint::Latest).unwrap();
assert_eq!(set.constraints.len(), 2);
}
#[test]
fn test_version_constraint_to_version_req_none() {
let latest_pre = VersionConstraint::LatestPrerelease;
let req = latest_pre.to_version_req().unwrap();
assert!(req.matches(&Version::parse("1.0.0").unwrap()));
assert!(req.matches(&Version::parse("0.1.0").unwrap()));
assert!(req.matches(&Version::parse("10.0.0").unwrap()));
}
}