use crate::{Error, ErrorKind::BadParam};
use semver::{Comparator, Op, Prerelease, Version};
use std::fmt::Display;
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) enum Bound {
Unbounded,
Exclusive(Version),
Inclusive(Version),
}
impl Bound {
pub fn version(&self) -> Option<&Version> {
match &self {
Bound::Unbounded => None,
Bound::Exclusive(v) => Some(v),
Bound::Inclusive(v) => Some(v),
}
}
fn less_or_equal(&self, other: &Bound) -> bool {
let start = self;
let end = other;
#[allow(clippy::if_same_then_else)]
if start == &Bound::Unbounded || end == &Bound::Unbounded {
true
} else if start.version().unwrap() < end.version().unwrap() {
true
} else {
match (&start, &end) {
(Bound::Inclusive(v_start), Bound::Inclusive(v_end)) => v_start == v_end,
(_, _) => false,
}
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) struct UnaffectedRange {
start: Bound,
end: Bound,
}
impl UnaffectedRange {
pub fn new(start: Bound, end: Bound) -> Result<Self, Error> {
if start.less_or_equal(&end) {
Ok(UnaffectedRange { start, end })
} else {
Err(format_err!(
BadParam,
"Invalid range: start must be <= end; if equal, both bounds must be inclusive"
))
}
}
pub fn start(&self) -> &Bound {
&self.start
}
pub fn end(&self) -> &Bound {
&self.end
}
pub fn overlaps(&self, other: &UnaffectedRange) -> bool {
self.start.less_or_equal(&other.end) && other.start.less_or_equal(&self.end)
}
}
impl Display for UnaffectedRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.start {
Bound::Unbounded => f.write_str("[0"),
Bound::Exclusive(v) => f.write_fmt(format_args!("({}", v)),
Bound::Inclusive(v) => f.write_fmt(format_args!("[{}", v)),
}?;
f.write_str(", ")?;
match &self.end {
Bound::Unbounded => f.write_str("∞)"),
Bound::Exclusive(v) => f.write_fmt(format_args!("{})", v)),
Bound::Inclusive(v) => f.write_fmt(format_args!("{}]", v)),
}
}
}
impl TryFrom<&semver::VersionReq> for UnaffectedRange {
type Error = Error;
fn try_from(input: &semver::VersionReq) -> Result<Self, Self::Error> {
if input.comparators.len() > 2 {
fail!(
BadParam,
format!("Too many comparators in version specification: {}", input)
);
}
let mut start = Bound::Unbounded;
let mut end = Bound::Unbounded;
for comparator in &input.comparators {
match comparator.op {
Op::Greater => {
if start != Bound::Unbounded {
fail!(
BadParam,
format!("More than one lower bound in the same range: {}", input)
);
}
start = Bound::Exclusive(comp_to_ver(comparator));
}
Op::GreaterEq => {
if start != Bound::Unbounded {
fail!(
BadParam,
format!("More than one lower bound in the same range: {}", input)
);
}
start = Bound::Inclusive(comp_to_ver(comparator));
}
Op::Less => {
if end != Bound::Unbounded {
fail!(
BadParam,
format!("More than one upper bound in the same range: {}", input)
);
}
end = Bound::Exclusive(comp_to_ver(comparator));
}
Op::LessEq => {
if end != Bound::Unbounded {
fail!(
BadParam,
format!("More than one upper bound in the same range: {}", input)
);
}
end = Bound::Inclusive(comp_to_ver(comparator));
}
Op::Exact => {
if input.comparators.len() != 1 {
fail!(BadParam, "Selectors that define an exact version (e.g. '=1.0') must be alone in their range");
}
start = Bound::Inclusive(comp_to_ver(comparator));
end = Bound::Inclusive(comp_to_ver(comparator));
}
Op::Caret => {
if input.comparators.len() != 1 {
fail!(BadParam, "Selectors that define both the upper and lower bound (e.g. '^1.0') must be alone in their range");
}
let start_version = comp_to_ver(comparator);
let mut end_version = if start_version.major == 0 {
match (comparator.minor, comparator.patch) {
(Some(0), Some(patch)) => Version::new(0, 0, patch + 1),
(Some(minor), _) => Version::new(0, minor + 1, 0),
(None, None) => Version::new(1, 0, 0),
(None, Some(_)) => unreachable!(
"Comparator specifies patch version but not minor version"
),
}
} else {
Version::new(&start_version.major + 1, 0, 0)
};
end_version.pre = Prerelease::new("0").unwrap();
start = Bound::Inclusive(start_version);
end = Bound::Exclusive(end_version);
}
Op::Tilde => {
if input.comparators.len() != 1 {
fail!(BadParam, "Selectors that define both the upper and lower bound (e.g. '~1.0') must be alone in their range");
}
let start_version = comp_to_ver(comparator);
let major = comparator.major;
let mut end_version = match (comparator.minor, comparator.patch) {
(None, None) => Version::new(major + 1, 0, 0),
(Some(minor), _) => Version::new(major, minor + 1, 0),
(None, Some(_)) => {
unreachable!("Comparator specifies patch version but not minor version")
}
};
end_version.pre = Prerelease::new("0").unwrap();
start = Bound::Inclusive(start_version);
end = Bound::Exclusive(end_version);
}
_ => {
fail!(
BadParam,
"Unsupported operator in version specification: '{}'",
comparator
);
}
}
}
UnaffectedRange::new(start, end)
}
}
fn comp_to_ver(c: &Comparator) -> Version {
Version {
major: c.major,
minor: c.minor.unwrap_or(0),
patch: c.patch.unwrap_or(0),
pre: c.pre.clone(),
build: Default::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use semver::VersionReq;
#[test]
fn both_unbounded() {
let range1 = UnaffectedRange {
start: Bound::Unbounded,
end: Bound::Unbounded,
};
let range2 = UnaffectedRange {
start: Bound::Unbounded,
end: Bound::Unbounded,
};
assert!(range1.overlaps(&range2));
assert!(range2.overlaps(&range1));
}
#[test]
fn barely_not_overlapping() {
let range1 = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.2.3").unwrap()),
end: Bound::Unbounded,
};
let range2 = UnaffectedRange {
start: Bound::Unbounded,
end: Bound::Exclusive(Version::parse("1.2.3").unwrap()),
};
assert!(!range1.overlaps(&range2));
assert!(!range2.overlaps(&range1));
}
#[test]
fn barely_overlapping() {
let range1 = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.2.3").unwrap()),
end: Bound::Unbounded,
};
let range2 = UnaffectedRange {
start: Bound::Unbounded,
end: Bound::Inclusive(Version::parse("1.2.3").unwrap()),
};
assert!(range1.overlaps(&range2));
assert!(range2.overlaps(&range1));
}
#[test]
fn clearly_not_overlapping() {
let range1 = UnaffectedRange {
start: Bound::Inclusive(Version::parse("0.1.0").unwrap()),
end: Bound::Inclusive(Version::parse("0.3.0").unwrap()),
};
let range2 = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.1.0").unwrap()),
end: Bound::Inclusive(Version::parse("1.3.0").unwrap()),
};
assert!(!range1.overlaps(&range2));
assert!(!range2.overlaps(&range1));
}
#[test]
fn clearly_overlapping() {
let range1 = UnaffectedRange {
start: Bound::Inclusive(Version::parse("0.1.0").unwrap()),
end: Bound::Inclusive(Version::parse("1.1.0").unwrap()),
};
let range2 = UnaffectedRange {
start: Bound::Inclusive(Version::parse("0.2.0").unwrap()),
end: Bound::Inclusive(Version::parse("1.3.0").unwrap()),
};
assert!(range1.overlaps(&range2));
assert!(range2.overlaps(&range1));
}
#[test]
fn exact_requirement_10() {
let input = VersionReq::parse("=1.0").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.0.0").unwrap()),
end: Bound::Inclusive(Version::parse("1.0.0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn caret_requirement_123() {
let input = VersionReq::parse("^1.2.3").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.2.3").unwrap()),
end: Bound::Exclusive(Version::parse("2.0.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn caret_requirement_12() {
let input = VersionReq::parse("^1.2").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.2.0").unwrap()),
end: Bound::Exclusive(Version::parse("2.0.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn caret_requirement_1() {
let input = VersionReq::parse("^1").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.0.0").unwrap()),
end: Bound::Exclusive(Version::parse("2.0.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn caret_requirement_023() {
let input = VersionReq::parse("^0.2.3").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("0.2.3").unwrap()),
end: Bound::Exclusive(Version::parse("0.3.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn caret_requirement_02() {
let input = VersionReq::parse("^0.2").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("0.2.0").unwrap()),
end: Bound::Exclusive(Version::parse("0.3.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn caret_requirement_003() {
let input = VersionReq::parse("^0.0.3").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("0.0.3").unwrap()),
end: Bound::Exclusive(Version::parse("0.0.4-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn caret_requirement_00() {
let input = VersionReq::parse("^0.0").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("0.0.0").unwrap()),
end: Bound::Exclusive(Version::parse("0.1.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn caret_requirement_0() {
let input = VersionReq::parse("^0").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("0.0.0").unwrap()),
end: Bound::Exclusive(Version::parse("1.0.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn tilde_requirement_123() {
let input = VersionReq::parse("~1.2.3").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.2.3").unwrap()),
end: Bound::Exclusive(Version::parse("1.3.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn tilde_requirement_12() {
let input = VersionReq::parse("~1.2").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.2.0").unwrap()),
end: Bound::Exclusive(Version::parse("1.3.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
#[test]
fn tilde_requirement_1() {
let input = VersionReq::parse("~1").unwrap();
let expected = UnaffectedRange {
start: Bound::Inclusive(Version::parse("1.0.0").unwrap()),
end: Bound::Exclusive(Version::parse("2.0.0-0").unwrap()),
};
let result: UnaffectedRange = (&input).try_into().unwrap();
assert_eq!(expected, result);
}
}