use crate::requirement::parser::parse_version_range;
use crate::version::RerVersion;
use core::fmt;
use std::ops::Bound;
use std::rc::Rc;
use version_ranges::Ranges;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct VersionRange(Rc<Ranges<RerVersion>>);
impl VersionRange {
pub fn any() -> Self {
VersionRange(Rc::new(Ranges::full()))
}
pub fn empty() -> Self {
VersionRange(Rc::new(Ranges::empty()))
}
pub fn parse(s: &str) -> Self {
let mut range: Option<Ranges<RerVersion>> = None;
for part in s.split('|') {
let part_range = parse_version_range(part);
range = Some(match range {
None => part_range,
Some(acc) => acc.union(&part_range),
});
}
VersionRange(Rc::new(range.unwrap_or_else(Ranges::full)))
}
pub fn from_ranges(ranges: Ranges<RerVersion>) -> Self {
VersionRange(Rc::new(ranges))
}
pub fn as_ranges(&self) -> &Ranges<RerVersion> {
&self.0
}
pub fn into_ranges(self) -> Ranges<RerVersion> {
Rc::unwrap_or_clone(self.0)
}
pub fn from_version(version: &RerVersion) -> Self {
VersionRange(Rc::new(Ranges::between(version.clone(), version.bump())))
}
pub fn from_versions<I: IntoIterator<Item = RerVersion>>(versions: I) -> Self {
VersionRange(Rc::new(
versions
.into_iter()
.map(|v| (Bound::Included(v.clone()), Bound::Included(v)))
.collect(),
))
}
pub fn is_any(&self) -> bool {
*self.0 == Ranges::full()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn intersection(&self, other: &Self) -> Option<Self> {
let result = self.0.intersection(&other.0);
if result.is_empty() {
None
} else {
Some(VersionRange(Rc::new(result)))
}
}
pub fn union(&self, other: &Self) -> Self {
VersionRange(Rc::new(self.0.union(&other.0)))
}
pub fn inverse(&self) -> Option<Self> {
if self.is_any() {
None
} else {
Some(VersionRange(Rc::new(self.0.complement())))
}
}
pub fn difference(&self, other: &Self) -> Option<Self> {
match other.inverse() {
None => None,
Some(inverse) => self.intersection(&inverse),
}
}
pub fn issuperset(&self, other: &Self) -> bool {
other.0.subset_of(&self.0)
}
pub fn issubset(&self, other: &Self) -> bool {
other.issuperset(self)
}
pub fn intersects(&self, other: &Self) -> bool {
!self.0.intersection(&other.0).is_empty()
}
pub fn contains(&self, version: &RerVersion) -> bool {
self.0.contains(version)
}
pub fn to_versions(&self) -> Option<Vec<RerVersion>> {
let mut versions = Vec::new();
for (lower, upper) in self.0.iter() {
if let (Bound::Included(low), Bound::Included(high)) = (lower, upper) {
if low == high {
versions.push(low.clone());
}
}
}
if versions.is_empty() {
None
} else {
Some(versions)
}
}
pub fn span(&self) -> Self {
match self.0.bounding_range() {
None => VersionRange::empty(),
Some((lower, upper)) => VersionRange(Rc::new(Ranges::from_range_bounds((
clone_bound(lower),
clone_bound(upper),
)))),
}
}
pub fn split(&self) -> Vec<Self> {
self.0
.iter()
.map(|(lower, upper)| {
VersionRange(Rc::new(Ranges::from_range_bounds((
lower.clone(),
upper.clone(),
))))
})
.collect()
}
}
fn clone_bound(bound: Bound<&RerVersion>) -> Bound<RerVersion> {
match bound {
Bound::Included(v) => Bound::Included(v.clone()),
Bound::Excluded(v) => Bound::Excluded(v.clone()),
Bound::Unbounded => Bound::Unbounded,
}
}
fn format_segment(lower: &Bound<RerVersion>, upper: &Bound<RerVersion>) -> String {
if matches!(upper, Bound::Unbounded) {
return match lower {
Bound::Unbounded => String::new(),
Bound::Included(v) => format!("{v}+"),
Bound::Excluded(v) => format!(">{v}"),
};
}
let (upper_version, upper_inclusive) = match upper {
Bound::Included(v) => (v, true),
Bound::Excluded(v) => (v, false),
Bound::Unbounded => unreachable!(),
};
let lower_version = match lower {
Bound::Included(v) | Bound::Excluded(v) => Some(v),
Bound::Unbounded => None,
};
let lower_inclusive = !matches!(lower, Bound::Excluded(_));
if lower_version == Some(upper_version) {
return format!("=={upper_version}");
}
if lower_inclusive && upper_inclusive {
return match lower_version {
Some(lv) => format!("{lv}..{upper_version}"),
None => format!("<={upper_version}"),
};
}
if lower_inclusive && !upper_inclusive {
if let Some(lv) = lower_version {
if &lv.bump() == upper_version {
return lv.to_string();
}
}
}
let lower_str = match lower {
Bound::Unbounded => String::new(),
Bound::Included(v) => format!("{v}+"),
Bound::Excluded(v) => format!(">{v}"),
};
let upper_str = if upper_inclusive {
format!("<={upper_version}")
} else {
format!("<{upper_version}")
};
format!("{lower_str}{upper_str}")
}
impl fmt::Display for VersionRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for (lower, upper) in self.0.iter() {
if !first {
write!(f, "|")?;
}
first = false;
write!(f, "{}", format_segment(lower, upper))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(s: &str) -> RerVersion {
RerVersion::try_from(s).unwrap()
}
#[test]
fn test_any_and_empty() {
assert!(VersionRange::any().is_any());
assert!(!VersionRange::any().is_empty());
assert!(VersionRange::empty().is_empty());
assert!(!VersionRange::empty().is_any());
assert!(VersionRange::parse("").is_any());
}
#[test]
fn test_intersection_none_when_disjoint() {
let a = VersionRange::parse("1+<2");
let b = VersionRange::parse("3+<4");
assert!(a.intersection(&b).is_none());
let c = VersionRange::parse("1.5+<3");
let hit = a.intersection(&c).unwrap();
assert!(hit.contains(&v("1.6")));
assert!(!hit.contains(&v("2.0")));
}
#[test]
fn test_inverse_none_only_for_any() {
assert!(VersionRange::any().inverse().is_none());
let inv = VersionRange::parse("2+<3").inverse().unwrap();
assert!(inv.contains(&v("1.0")));
assert!(inv.contains(&v("4.0")));
assert!(!inv.contains(&v("2.5")));
}
#[test]
fn test_difference() {
let a = VersionRange::parse("1+<5");
let b = VersionRange::parse("2+<3");
let diff = a.difference(&b).unwrap();
assert!(diff.contains(&v("1.5")));
assert!(!diff.contains(&v("2.5")));
assert!(diff.contains(&v("3.0")));
assert!(a.difference(&VersionRange::any()).is_none());
assert!(b.difference(&a).is_none());
}
#[test]
fn test_superset_subset_intersects() {
let wide = VersionRange::parse("1+<10");
let narrow = VersionRange::parse("2+<3");
assert!(wide.issuperset(&narrow));
assert!(!narrow.issuperset(&wide));
assert!(narrow.issubset(&wide));
assert!(wide.intersects(&narrow));
assert!(!narrow.intersects(&VersionRange::parse("5+<6")));
}
#[test]
fn test_from_version_superset() {
let r = VersionRange::from_version(&v("3"));
assert!(r.contains(&v("3")));
assert!(r.contains(&v("3.0")));
assert!(r.contains(&v("3.1.4")));
assert!(!r.contains(&v("4")));
}
#[test]
fn test_from_versions_and_to_versions() {
let r = VersionRange::from_versions([v("3"), v("5.1"), v("4")]);
let mut versions = r.to_versions().unwrap();
versions.sort();
assert_eq!(versions, vec![v("3"), v("4"), v("5.1")]);
assert!(VersionRange::parse("1+<2").to_versions().is_none());
}
#[test]
fn test_display_rez_format() {
assert_eq!(VersionRange::parse("4").to_string(), "4");
assert_eq!(VersionRange::parse("1.0").to_string(), "1.0");
assert_eq!(VersionRange::parse("3+<5").to_string(), "3+<5");
assert_eq!(VersionRange::parse("3+").to_string(), "3+");
assert_eq!(VersionRange::parse("<3").to_string(), "<3");
assert_eq!(VersionRange::parse("==1.0.1").to_string(), "==1.0.1");
assert_eq!(VersionRange::parse("2|5").to_string(), "2|5");
assert_eq!(VersionRange::any().to_string(), "");
}
#[test]
fn test_span_and_split() {
let r = VersionRange::parse("2+<4|6+<8");
let span = r.span();
assert!(span.contains(&v("5")));
assert!(span.contains(&v("3")));
assert!(!span.contains(&v("8")));
let parts = r.split();
assert_eq!(parts.len(), 2);
assert!(parts[0].contains(&v("3")));
assert!(!parts[0].contains(&v("6")));
assert!(parts[1].contains(&v("6")));
}
}