use crate::Version;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum ConstraintSpec {
Exact(Version),
Range { lower_inclusive: Version, upper_exclusive: Version },
Tilde(Version),
Caret(Version),
GreaterEqual(Version),
Greater(Version),
LessEqual(Version),
Less(Version),
Any,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompoundConstraint {
pub combinator: Combinator,
pub atoms: Vec<ConstraintSpec>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Combinator {
Or,
And,
}
impl CompoundConstraint {
#[must_use]
pub fn matches(&self, v: &Version) -> bool {
match self.combinator {
Combinator::Or => self.atoms.iter().any(|a| match_constraint(a, v)),
Combinator::And => self.atoms.iter().all(|a| match_constraint(a, v)),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VersionConstraint {
pub spec: ConstraintSpec,
#[serde(default)]
pub native_syntax: Option<String>,
}
impl VersionConstraint {
#[must_use]
pub const fn from_spec(spec: ConstraintSpec) -> Self {
Self {
spec,
native_syntax: None,
}
}
#[must_use]
pub fn matches(&self, v: &Version) -> bool {
match_constraint(&self.spec, v)
}
}
fn match_constraint(c: &ConstraintSpec, v: &Version) -> bool {
match c {
ConstraintSpec::Exact(target) => v == target,
ConstraintSpec::Range { lower_inclusive, upper_exclusive } => {
v >= lower_inclusive && v < upper_exclusive
}
ConstraintSpec::Tilde(base) => {
v >= base
&& v < &Version::new(base.major, base.minor + 1, 0)
}
ConstraintSpec::Caret(base) => {
if base.major > 0 {
v >= base && v < &Version::new(base.major + 1, 0, 0)
} else if base.minor > 0 {
v >= base && v < &Version::new(0, base.minor + 1, 0)
} else {
v >= base && v < &Version::new(0, 0, base.patch + 1)
}
}
ConstraintSpec::GreaterEqual(target) => v >= target,
ConstraintSpec::Greater(target) => v > target,
ConstraintSpec::LessEqual(target) => v <= target,
ConstraintSpec::Less(target) => v < target,
ConstraintSpec::Any => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_matches_only_exact_version() {
let c = VersionConstraint::from_spec(ConstraintSpec::Exact(Version::new(1, 2, 3)));
assert!(c.matches(&Version::new(1, 2, 3)));
assert!(!c.matches(&Version::new(1, 2, 4)));
}
#[test]
fn caret_major_above_zero() {
let c = VersionConstraint::from_spec(ConstraintSpec::Caret(Version::new(1, 2, 3)));
assert!(c.matches(&Version::new(1, 2, 3)));
assert!(c.matches(&Version::new(1, 99, 0)));
assert!(!c.matches(&Version::new(2, 0, 0)));
assert!(!c.matches(&Version::new(1, 2, 2)));
}
#[test]
fn caret_major_zero_minor_above_zero() {
let c = VersionConstraint::from_spec(ConstraintSpec::Caret(Version::new(0, 2, 3)));
assert!(c.matches(&Version::new(0, 2, 3)));
assert!(c.matches(&Version::new(0, 2, 99)));
assert!(!c.matches(&Version::new(0, 3, 0)));
}
#[test]
fn caret_major_and_minor_zero() {
let c = VersionConstraint::from_spec(ConstraintSpec::Caret(Version::new(0, 0, 3)));
assert!(c.matches(&Version::new(0, 0, 3)));
assert!(!c.matches(&Version::new(0, 0, 4)));
}
#[test]
fn tilde_only_allows_patch_changes() {
let c = VersionConstraint::from_spec(ConstraintSpec::Tilde(Version::new(1, 2, 3)));
assert!(c.matches(&Version::new(1, 2, 3)));
assert!(c.matches(&Version::new(1, 2, 99)));
assert!(!c.matches(&Version::new(1, 3, 0)));
}
#[test]
fn range_inclusive_lower_exclusive_upper() {
let c = VersionConstraint::from_spec(ConstraintSpec::Range {
lower_inclusive: Version::new(1, 2, 3),
upper_exclusive: Version::new(2, 0, 0),
});
assert!(c.matches(&Version::new(1, 2, 3)));
assert!(c.matches(&Version::new(1, 99, 99)));
assert!(!c.matches(&Version::new(2, 0, 0)));
assert!(!c.matches(&Version::new(1, 2, 2)));
}
#[test]
fn open_bounds() {
let ge = VersionConstraint::from_spec(ConstraintSpec::GreaterEqual(Version::new(1, 0, 0)));
let gt = VersionConstraint::from_spec(ConstraintSpec::Greater(Version::new(1, 0, 0)));
let le = VersionConstraint::from_spec(ConstraintSpec::LessEqual(Version::new(1, 0, 0)));
let lt = VersionConstraint::from_spec(ConstraintSpec::Less(Version::new(1, 0, 0)));
assert!(ge.matches(&Version::new(1, 0, 0)));
assert!(!gt.matches(&Version::new(1, 0, 0)));
assert!(le.matches(&Version::new(1, 0, 0)));
assert!(!lt.matches(&Version::new(1, 0, 0)));
}
#[test]
fn any_matches_everything() {
let c = VersionConstraint::from_spec(ConstraintSpec::Any);
assert!(c.matches(&Version::new(0, 0, 0)));
assert!(c.matches(&Version::new(999, 999, 999)));
}
#[test]
fn disjunction_via_compound() {
let c = CompoundConstraint {
combinator: Combinator::Or,
atoms: vec![
ConstraintSpec::Caret(Version::new(1, 0, 0)),
ConstraintSpec::Caret(Version::new(2, 0, 0)),
],
};
assert!(c.matches(&Version::new(1, 5, 0)));
assert!(c.matches(&Version::new(2, 5, 0)));
assert!(!c.matches(&Version::new(3, 0, 0)));
}
#[test]
fn conjunction_via_compound() {
let c = CompoundConstraint {
combinator: Combinator::And,
atoms: vec![
ConstraintSpec::GreaterEqual(Version::new(1, 2, 0)),
ConstraintSpec::Less(Version::new(2, 0, 0)),
],
};
assert!(c.matches(&Version::new(1, 5, 0)));
assert!(!c.matches(&Version::new(1, 1, 0)));
assert!(!c.matches(&Version::new(2, 0, 0)));
}
#[test]
fn round_trip_through_serde() {
let c = VersionConstraint {
spec: ConstraintSpec::Caret(Version::new(1, 2, 3)),
native_syntax: Some("^1.2.3".to_string()),
};
let j = serde_json::to_string(&c).unwrap();
let parsed: VersionConstraint = serde_json::from_str(&j).unwrap();
assert_eq!(c, parsed);
}
}