use std::{convert::TryFrom, fmt::Display};
use semver::{Comparator, Op, Prerelease, Version};
use crate::{Error, ErrorKind::BadParam};
#[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::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 {
Version::new(0, start_version.minor + 1, 0)
} 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);
}
_ => {
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::*;
#[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));
}
}