use anyhow::Result;
use semver::Version;
use super::VersionConstraint;
use crate::core::AgpmError;
#[derive(Debug, Clone)]
pub struct ConstraintSet {
constraints: Vec<VersionConstraint>,
}
impl Default for ConstraintSet {
fn default() -> Self {
Self::new()
}
}
impl ConstraintSet {
#[must_use]
pub const fn new() -> Self {
Self {
constraints: Vec::new(),
}
}
pub fn add(&mut self, constraint: VersionConstraint) -> Result<()> {
if self.has_conflict(&constraint) {
return Err(AgpmError::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 {
prefix: p1,
version: v1,
},
VersionConstraint::Exact {
prefix: p2,
version: v2,
},
) => {
if p1 != p2 {
continue;
}
if v1 != v2 {
return true;
}
}
(VersionConstraint::GitRef(r1), VersionConstraint::GitRef(r2)) => {
if r1 != r2 {
return true;
}
}
(
VersionConstraint::Exact {
prefix: p1,
..
}
| VersionConstraint::Requirement {
prefix: p1,
..
},
VersionConstraint::Requirement {
prefix: p2,
..
},
)
| (
VersionConstraint::Requirement {
prefix: p1,
..
},
VersionConstraint::Exact {
prefix: p2,
..
},
) => {
if p1 != p2 {
}
}
_ => {
}
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use semver::Version;
#[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() -> Result<()> {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::Exact {
prefix: None,
version: Version::parse("1.0.0").unwrap(),
})
.unwrap();
let result = set.add(VersionConstraint::Exact {
prefix: None,
version: Version::parse("2.0.0").unwrap(),
});
assert!(result.is_err());
let result = set.add(VersionConstraint::Exact {
prefix: None,
version: Version::parse("1.0.0").unwrap(),
});
result?;
Ok(())
}
#[test]
fn test_allows_prerelease() {
assert!(VersionConstraint::GitRef("main".to_string()).allows_prerelease());
assert!(VersionConstraint::GitRef("latest".to_string()).allows_prerelease()); assert!(
!VersionConstraint::Exact {
prefix: None,
version: Version::parse("1.0.0").unwrap()
}
.allows_prerelease()
);
}
#[test]
fn test_constraint_set_with_prereleases() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::GitRef("main".to_string())).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);
assert!(best.is_none()); }
#[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_set_git_ref_conflicts() -> Result<()> {
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()));
result?;
Ok(())
}
#[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_constraint_set_no_conflict_different_types() {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
assert_eq!(set.constraints.len(), 2);
}
#[test]
fn test_prefixed_constraint_conflicts() -> Result<()> {
let mut set = ConstraintSet::new();
set.add(VersionConstraint::parse("agents-^v1.0.0").unwrap()).unwrap();
let result = set.add(VersionConstraint::parse("snippets-^v1.0.0").unwrap());
result?;
let result = set.add(VersionConstraint::parse("agents-~v1.2.0").unwrap());
result?;
let mut exact_set = ConstraintSet::new();
exact_set.add(VersionConstraint::parse("agents-v1.0.0").unwrap()).unwrap();
let result = exact_set.add(VersionConstraint::parse("snippets-v1.0.0").unwrap());
result?;
Ok(())
}
}