Skip to main content

rer_version/
range.rs

1//! Faithful port of rez's `VersionRange` semantics.
2//!
3//! rez's solver relies on a specific contract that `version_ranges::Ranges`
4//! does not express directly — chiefly that `intersection`, the `-` operator
5//! and `inverse` return `None` to signal "empty / no result", and that there
6//! is a distinguished "any" range. This module wraps `Ranges<RerVersion>` and
7//! re-exposes rez's API (`rez/src/rez/version/_version.py`, class
8//! `VersionRange`) so the rest of the solver port can mirror the Python code
9//! closely.
10
11use crate::requirement::parser::parse_version_range;
12use crate::version::RerVersion;
13use core::fmt;
14use std::ops::Bound;
15use version_ranges::Ranges;
16
17/// A set of one or more contiguous version ranges, with rez's semantics.
18///
19/// Construct from a rez range string with [`VersionRange::parse`], from a
20/// single version with [`VersionRange::from_version`], or from an existing
21/// [`Ranges`] with [`VersionRange::from_ranges`].
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct VersionRange(Ranges<RerVersion>);
24
25impl VersionRange {
26    /// The "any" range — contains every version. Mirrors rez's empty-string range.
27    pub fn any() -> Self {
28        VersionRange(Ranges::full())
29    }
30
31    /// The empty range — contains no version. rez represents "empty" as `None`
32    /// at the API boundary, but an empty `VersionRange` is still useful
33    /// internally (e.g. as a `from_versions` accumulator).
34    pub fn empty() -> Self {
35        VersionRange(Ranges::empty())
36    }
37
38    /// Parse a rez range string such as `"3"`, `"1+<2"`, `"==1.0"`, `"2|6+"`.
39    ///
40    /// `|`-separated alternatives are unioned together. The empty string is the
41    /// "any" range.
42    ///
43    /// # Panics
44    ///
45    /// Panics on a syntactically invalid range string, matching the existing
46    /// `parse_version_range` behaviour.
47    pub fn parse(s: &str) -> Self {
48        let mut range: Option<Ranges<RerVersion>> = None;
49        for part in s.split('|') {
50            let part_range = parse_version_range(part);
51            range = Some(match range {
52                None => part_range,
53                Some(acc) => acc.union(&part_range),
54            });
55        }
56        VersionRange(range.unwrap_or_else(Ranges::full))
57    }
58
59    /// Wrap a raw [`Ranges`] (already produced by the requirement parser).
60    pub fn from_ranges(ranges: Ranges<RerVersion>) -> Self {
61        VersionRange(ranges)
62    }
63
64    /// Borrow the underlying [`Ranges`].
65    pub fn as_ranges(&self) -> &Ranges<RerVersion> {
66        &self.0
67    }
68
69    /// Consume into the underlying [`Ranges`].
70    pub fn into_ranges(self) -> Ranges<RerVersion> {
71        self.0
72    }
73
74    /// rez `VersionRange.from_version` with `op=None`: the "superset" range
75    /// `[version, version.next())`, e.g. `3` contains `3`, `3.0`, `3.1.4`.
76    pub fn from_version(version: &RerVersion) -> Self {
77        VersionRange(Ranges::between(version.clone(), version.bump()))
78    }
79
80    /// rez `VersionRange.from_versions`: a range containing exactly the given
81    /// versions and nothing else (e.g. `==3|==4|==5.1`).
82    ///
83    /// Built in a single pass via `Ranges`'s `FromIterator` rather than folding
84    /// `union` over singletons (which reallocated and was O(n²)) — this is hot,
85    /// it backs `_PackageScope._update`.
86    pub fn from_versions<I: IntoIterator<Item = RerVersion>>(versions: I) -> Self {
87        VersionRange(
88            versions
89                .into_iter()
90                .map(|v| (Bound::Included(v.clone()), Bound::Included(v)))
91                .collect(),
92        )
93    }
94
95    /// rez `VersionRange.is_any`: true for the range that contains all versions.
96    pub fn is_any(&self) -> bool {
97        self.0 == Ranges::full()
98    }
99
100    /// True if this range contains no version.
101    pub fn is_empty(&self) -> bool {
102        self.0.is_empty()
103    }
104
105    /// rez `VersionRange.intersection` (`&`): `None` if the ranges are disjoint.
106    pub fn intersection(&self, other: &Self) -> Option<Self> {
107        let result = self.0.intersection(&other.0);
108        if result.is_empty() {
109            None
110        } else {
111            Some(VersionRange(result))
112        }
113    }
114
115    /// rez `VersionRange.union` (`|`): always succeeds.
116    pub fn union(&self, other: &Self) -> Self {
117        VersionRange(self.0.union(&other.0))
118    }
119
120    /// rez `VersionRange.inverse` (`~`): `None` if and only if this is the
121    /// "any" range (whose inverse would be empty).
122    pub fn inverse(&self) -> Option<Self> {
123        if self.is_any() {
124            None
125        } else {
126            Some(VersionRange(self.0.complement()))
127        }
128    }
129
130    /// rez `VersionRange.__sub__` (`-`): `self & ~other`. `None` if `other` is
131    /// the "any" range, or if the result is empty.
132    pub fn difference(&self, other: &Self) -> Option<Self> {
133        match other.inverse() {
134            None => None,
135            Some(inverse) => self.intersection(&inverse),
136        }
137    }
138
139    /// rez `VersionRange.issuperset`: true if `other` is fully contained here.
140    pub fn issuperset(&self, other: &Self) -> bool {
141        other.0.subset_of(&self.0)
142    }
143
144    /// rez `VersionRange.issubset`: true if `self` is fully contained in `other`.
145    pub fn issubset(&self, other: &Self) -> bool {
146        other.issuperset(self)
147    }
148
149    /// rez `VersionRange.intersects`: true if the ranges share any version.
150    pub fn intersects(&self, other: &Self) -> bool {
151        !self.0.intersection(&other.0).is_empty()
152    }
153
154    /// rez `VersionRange.contains_version` / `version in range`.
155    pub fn contains(&self, version: &RerVersion) -> bool {
156        self.0.contains(version)
157    }
158
159    /// rez `VersionRange.to_versions`: the exact (single-version) bounds present
160    /// in this range, or `None` if there are none. Non-exact bounds are ignored,
161    /// matching rez.
162    pub fn to_versions(&self) -> Option<Vec<RerVersion>> {
163        let mut versions = Vec::new();
164        for (lower, upper) in self.0.iter() {
165            if let (Bound::Included(low), Bound::Included(high)) = (lower, upper) {
166                if low == high {
167                    versions.push(low.clone());
168                }
169            }
170        }
171        if versions.is_empty() {
172            None
173        } else {
174            Some(versions)
175        }
176    }
177
178    /// rez `VersionRange.span`: the smallest contiguous range that is a superset
179    /// of this range (e.g. the span of `2+<4|6+<8` is `2+<8`).
180    pub fn span(&self) -> Self {
181        match self.0.bounding_range() {
182            None => VersionRange::empty(),
183            Some((lower, upper)) => VersionRange(Ranges::from_range_bounds((
184                clone_bound(lower),
185                clone_bound(upper),
186            ))),
187        }
188    }
189
190    /// rez `VersionRange.split`: one [`VersionRange`] per contiguous sub-range
191    /// (e.g. `3|5+` splits into `["3", "5+"]`).
192    pub fn split(&self) -> Vec<Self> {
193        self.0
194            .iter()
195            .map(|(lower, upper)| {
196                VersionRange(Ranges::from_range_bounds((lower.clone(), upper.clone())))
197            })
198            .collect()
199    }
200}
201
202/// Clone a `Bound<&V>` into an owned `Bound<V>`.
203fn clone_bound(bound: Bound<&RerVersion>) -> Bound<RerVersion> {
204    match bound {
205        Bound::Included(v) => Bound::Included(v.clone()),
206        Bound::Excluded(v) => Bound::Excluded(v.clone()),
207        Bound::Unbounded => Bound::Unbounded,
208    }
209}
210
211/// Format one contiguous segment in rez's range syntax, mirroring rez's
212/// `_Bound.__str__` (`rez/src/rez/version/_version.py:506`).
213fn format_segment(lower: &Bound<RerVersion>, upper: &Bound<RerVersion>) -> String {
214    // 1. Infinite upper bound -> just the lower bound string.
215    if matches!(upper, Bound::Unbounded) {
216        return match lower {
217            Bound::Unbounded => String::new(),
218            Bound::Included(v) => format!("{v}+"),
219            Bound::Excluded(v) => format!(">{v}"),
220        };
221    }
222    let (upper_version, upper_inclusive) = match upper {
223        Bound::Included(v) => (v, true),
224        Bound::Excluded(v) => (v, false),
225        Bound::Unbounded => unreachable!(),
226    };
227    let lower_version = match lower {
228        Bound::Included(v) | Bound::Excluded(v) => Some(v),
229        Bound::Unbounded => None,
230    };
231    let lower_inclusive = !matches!(lower, Bound::Excluded(_));
232
233    // 2. Single exact version -> "==v".
234    if lower_version == Some(upper_version) {
235        return format!("=={upper_version}");
236    }
237    // 3. Both bounds inclusive.
238    if lower_inclusive && upper_inclusive {
239        return match lower_version {
240            Some(lv) => format!("{lv}..{upper_version}"),
241            None => format!("<={upper_version}"),
242        };
243    }
244    // 4. The "superset" form: [v, v.next()) prints as just "v".
245    if lower_inclusive && !upper_inclusive {
246        if let Some(lv) = lower_version {
247            if &lv.bump() == upper_version {
248                return lv.to_string();
249            }
250        }
251    }
252    // 5. General case: lower string followed by upper string.
253    let lower_str = match lower {
254        Bound::Unbounded => String::new(),
255        Bound::Included(v) => format!("{v}+"),
256        Bound::Excluded(v) => format!(">{v}"),
257    };
258    let upper_str = if upper_inclusive {
259        format!("<={upper_version}")
260    } else {
261        format!("<{upper_version}")
262    };
263    format!("{lower_str}{upper_str}")
264}
265
266impl fmt::Display for VersionRange {
267    /// Renders in rez's compact range syntax (`3`, `3+<5`, `==1.0`, `2|6+`).
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        let mut first = true;
270        for (lower, upper) in self.0.iter() {
271            if !first {
272                write!(f, "|")?;
273            }
274            first = false;
275            write!(f, "{}", format_segment(lower, upper))?;
276        }
277        Ok(())
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    fn v(s: &str) -> RerVersion {
286        RerVersion::try_from(s).unwrap()
287    }
288
289    #[test]
290    fn test_any_and_empty() {
291        assert!(VersionRange::any().is_any());
292        assert!(!VersionRange::any().is_empty());
293        assert!(VersionRange::empty().is_empty());
294        assert!(!VersionRange::empty().is_any());
295        // The parsed empty-string range is the "any" range, per rez.
296        assert!(VersionRange::parse("").is_any());
297    }
298
299    #[test]
300    fn test_intersection_none_when_disjoint() {
301        let a = VersionRange::parse("1+<2");
302        let b = VersionRange::parse("3+<4");
303        assert!(a.intersection(&b).is_none());
304        let c = VersionRange::parse("1.5+<3");
305        let hit = a.intersection(&c).unwrap();
306        assert!(hit.contains(&v("1.6")));
307        assert!(!hit.contains(&v("2.0")));
308    }
309
310    #[test]
311    fn test_inverse_none_only_for_any() {
312        assert!(VersionRange::any().inverse().is_none());
313        let inv = VersionRange::parse("2+<3").inverse().unwrap();
314        assert!(inv.contains(&v("1.0")));
315        assert!(inv.contains(&v("4.0")));
316        assert!(!inv.contains(&v("2.5")));
317    }
318
319    #[test]
320    fn test_difference() {
321        // a - b = a & ~b
322        let a = VersionRange::parse("1+<5");
323        let b = VersionRange::parse("2+<3");
324        let diff = a.difference(&b).unwrap();
325        assert!(diff.contains(&v("1.5")));
326        assert!(!diff.contains(&v("2.5")));
327        assert!(diff.contains(&v("3.0")));
328        // subtracting "any" yields None
329        assert!(a.difference(&VersionRange::any()).is_none());
330        // subtracting a superset empties the result -> None
331        assert!(b.difference(&a).is_none());
332    }
333
334    #[test]
335    fn test_superset_subset_intersects() {
336        let wide = VersionRange::parse("1+<10");
337        let narrow = VersionRange::parse("2+<3");
338        assert!(wide.issuperset(&narrow));
339        assert!(!narrow.issuperset(&wide));
340        assert!(narrow.issubset(&wide));
341        assert!(wide.intersects(&narrow));
342        assert!(!narrow.intersects(&VersionRange::parse("5+<6")));
343    }
344
345    #[test]
346    fn test_from_version_superset() {
347        // rez: the range `3` is the superset of any `3[.X.X...]`.
348        let r = VersionRange::from_version(&v("3"));
349        assert!(r.contains(&v("3")));
350        assert!(r.contains(&v("3.0")));
351        assert!(r.contains(&v("3.1.4")));
352        assert!(!r.contains(&v("4")));
353    }
354
355    #[test]
356    fn test_from_versions_and_to_versions() {
357        let r = VersionRange::from_versions([v("3"), v("5.1"), v("4")]);
358        let mut versions = r.to_versions().unwrap();
359        versions.sort();
360        assert_eq!(versions, vec![v("3"), v("4"), v("5.1")]);
361        // a non-exact range has no exact versions
362        assert!(VersionRange::parse("1+<2").to_versions().is_none());
363    }
364
365    #[test]
366    fn test_display_rez_format() {
367        // Compact rez range syntax, not the underlying Ranges debug format.
368        assert_eq!(VersionRange::parse("4").to_string(), "4");
369        assert_eq!(VersionRange::parse("1.0").to_string(), "1.0");
370        assert_eq!(VersionRange::parse("3+<5").to_string(), "3+<5");
371        assert_eq!(VersionRange::parse("3+").to_string(), "3+");
372        assert_eq!(VersionRange::parse("<3").to_string(), "<3");
373        assert_eq!(VersionRange::parse("==1.0.1").to_string(), "==1.0.1");
374        assert_eq!(VersionRange::parse("2|5").to_string(), "2|5");
375        assert_eq!(VersionRange::any().to_string(), "");
376    }
377
378    #[test]
379    fn test_span_and_split() {
380        let r = VersionRange::parse("2+<4|6+<8");
381        let span = r.span();
382        assert!(span.contains(&v("5")));
383        assert!(span.contains(&v("3")));
384        assert!(!span.contains(&v("8")));
385        let parts = r.split();
386        assert_eq!(parts.len(), 2);
387        assert!(parts[0].contains(&v("3")));
388        assert!(!parts[0].contains(&v("6")));
389        assert!(parts[1].contains(&v("6")));
390    }
391}