Skip to main content

pro_core/semver/
range.rs

1//! SemVer range parsing and matching
2//!
3//! Supports npm/Cargo-style version ranges:
4//! - Exact: `=1.2.3`, `1.2.3`
5//! - Comparison: `>1.0.0`, `>=1.0.0`, `<2.0.0`, `<=2.0.0`
6//! - Caret: `^1.2.3` (compatible with 1.x.x, >=1.2.3 <2.0.0)
7//! - Tilde: `~1.2.3` (compatible with 1.2.x, >=1.2.3 <1.3.0)
8//! - Wildcard: `1.*`, `1.2.*`, `*`
9//! - Hyphen: `1.0.0 - 2.0.0` (>=1.0.0 <=2.0.0)
10//! - OR: `^1.0.0 || ^2.0.0`
11
12use std::fmt;
13use std::str::FromStr;
14
15use crate::semver::Version;
16use crate::Error;
17
18/// A version requirement (set of ranges combined with OR)
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct VersionReq {
21    /// Ranges combined with OR (any match satisfies)
22    pub ranges: Vec<Range>,
23}
24
25impl VersionReq {
26    /// Matches any version
27    pub const STAR: Self = Self { ranges: Vec::new() };
28
29    /// Parse a version requirement string
30    pub fn parse(text: &str) -> Result<Self, Error> {
31        let text = text.trim();
32
33        if text.is_empty() || text == "*" {
34            return Ok(Self::STAR);
35        }
36
37        // Split by || for OR combinations
38        let ranges: Result<Vec<_>, _> = text.split("||").map(|s| Range::parse(s.trim())).collect();
39
40        Ok(Self { ranges: ranges? })
41    }
42
43    /// Check if a version satisfies this requirement
44    pub fn matches(&self, version: &Version) -> bool {
45        // Empty ranges means * (match all)
46        if self.ranges.is_empty() {
47            return true;
48        }
49
50        // Any range matching is sufficient (OR)
51        self.ranges.iter().any(|r| r.matches(version))
52    }
53
54    /// Check if this matches any version
55    pub fn is_any(&self) -> bool {
56        self.ranges.is_empty()
57    }
58}
59
60impl fmt::Display for VersionReq {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        if self.ranges.is_empty() {
63            return write!(f, "*");
64        }
65
66        let mut first = true;
67        for range in &self.ranges {
68            if !first {
69                write!(f, " || ")?;
70            }
71            first = false;
72            write!(f, "{}", range)?;
73        }
74        Ok(())
75    }
76}
77
78impl FromStr for VersionReq {
79    type Err = Error;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        Self::parse(s)
83    }
84}
85
86impl Default for VersionReq {
87    fn default() -> Self {
88        Self::STAR
89    }
90}
91
92/// A range of versions (comparators combined with AND)
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct Range {
95    /// Comparators combined with AND (all must match)
96    pub comparators: Vec<Comparator>,
97}
98
99impl Range {
100    /// Parse a range string (space or comma separated comparators)
101    pub fn parse(text: &str) -> Result<Self, Error> {
102        let text = text.trim();
103
104        if text.is_empty() || text == "*" {
105            return Ok(Self {
106                comparators: vec![],
107            });
108        }
109
110        // Check for hyphen range first: "1.0.0 - 2.0.0"
111        if let Some(idx) = text.find(" - ") {
112            let lower = text[..idx].trim();
113            let upper = text[idx + 3..].trim();
114            return Self::parse_hyphen_range(lower, upper);
115        }
116
117        // Split by whitespace or comma for AND combinations
118        let parts: Vec<&str> = text
119            .split(|c: char| c.is_whitespace() || c == ',')
120            .filter(|s| !s.is_empty())
121            .collect();
122
123        let comparators: Result<Vec<_>, _> = parts.iter().map(|s| Comparator::parse(s)).collect();
124
125        Ok(Self {
126            comparators: comparators?,
127        })
128    }
129
130    /// Parse hyphen range: "1.0.0 - 2.0.0" => >=1.0.0 <=2.0.0
131    fn parse_hyphen_range(lower: &str, upper: &str) -> Result<Self, Error> {
132        let lower_version = Version::parse(lower)?;
133        let upper_version = Version::parse(upper)?;
134
135        Ok(Self {
136            comparators: vec![
137                Comparator {
138                    op: Op::GreaterEq,
139                    major: lower_version.major,
140                    minor: Some(lower_version.minor),
141                    patch: Some(lower_version.patch),
142                    pre: lower_version.pre,
143                },
144                Comparator {
145                    op: Op::LessEq,
146                    major: upper_version.major,
147                    minor: Some(upper_version.minor),
148                    patch: Some(upper_version.patch),
149                    pre: upper_version.pre,
150                },
151            ],
152        })
153    }
154
155    /// Check if a version satisfies this range
156    pub fn matches(&self, version: &Version) -> bool {
157        // Empty comparators means * (match all)
158        if self.comparators.is_empty() {
159            return true;
160        }
161
162        // All comparators must match (AND)
163        self.comparators.iter().all(|c| c.matches(version))
164    }
165}
166
167impl fmt::Display for Range {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        if self.comparators.is_empty() {
170            return write!(f, "*");
171        }
172
173        let mut first = true;
174        for comp in &self.comparators {
175            if !first {
176                write!(f, " ")?;
177            }
178            first = false;
179            write!(f, "{}", comp)?;
180        }
181        Ok(())
182    }
183}
184
185impl FromStr for Range {
186    type Err = Error;
187
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        Self::parse(s)
190    }
191}
192
193/// Comparison operator
194#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
195pub enum Op {
196    /// `=` or implied - exact match
197    Exact,
198    /// `>` - greater than
199    Greater,
200    /// `>=` - greater than or equal
201    GreaterEq,
202    /// `<` - less than
203    Less,
204    /// `<=` - less than or equal
205    LessEq,
206    /// `^` - caret (compatible)
207    Caret,
208    /// `~` - tilde (patch-level compatible)
209    Tilde,
210    /// `*` - wildcard
211    Wildcard,
212}
213
214impl fmt::Display for Op {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        match self {
217            Op::Exact => Ok(()),
218            Op::Greater => write!(f, ">"),
219            Op::GreaterEq => write!(f, ">="),
220            Op::Less => write!(f, "<"),
221            Op::LessEq => write!(f, "<="),
222            Op::Caret => write!(f, "^"),
223            Op::Tilde => write!(f, "~"),
224            Op::Wildcard => write!(f, "*"),
225        }
226    }
227}
228
229/// A single version comparator
230#[derive(Clone, Debug, PartialEq, Eq)]
231pub struct Comparator {
232    /// Comparison operator
233    pub op: Op,
234    /// Major version
235    pub major: u64,
236    /// Minor version (None for wildcards like 1.*)
237    pub minor: Option<u64>,
238    /// Patch version (None for wildcards like 1.2.*)
239    pub patch: Option<u64>,
240    /// Prerelease
241    pub pre: crate::semver::Prerelease,
242}
243
244impl Comparator {
245    /// Parse a single comparator
246    pub fn parse(text: &str) -> Result<Self, Error> {
247        let text = text.trim();
248
249        if text.is_empty() {
250            return Err(Error::InvalidVersion("empty comparator".to_string()));
251        }
252
253        // Handle pure wildcard
254        if text == "*" || text == "x" || text == "X" {
255            return Ok(Self {
256                op: Op::Wildcard,
257                major: 0,
258                minor: None,
259                patch: None,
260                pre: crate::semver::Prerelease::EMPTY,
261            });
262        }
263
264        // Parse operator prefix
265        let (op, rest) = if let Some(rest) = text.strip_prefix(">=") {
266            (Op::GreaterEq, rest)
267        } else if let Some(rest) = text.strip_prefix("<=") {
268            (Op::LessEq, rest)
269        } else if let Some(rest) = text.strip_prefix('>') {
270            (Op::Greater, rest)
271        } else if let Some(rest) = text.strip_prefix('<') {
272            (Op::Less, rest)
273        } else if let Some(rest) = text.strip_prefix('^') {
274            (Op::Caret, rest)
275        } else if let Some(rest) = text.strip_prefix('~') {
276            (Op::Tilde, rest)
277        } else if let Some(rest) = text.strip_prefix('=') {
278            (Op::Exact, rest)
279        } else {
280            (Op::Exact, text)
281        };
282
283        let rest = rest.trim();
284
285        // Handle wildcards in version
286        if rest.contains('*') || rest.contains('x') || rest.contains('X') {
287            return Self::parse_wildcard(rest);
288        }
289
290        // Parse the version part
291        let version = Version::parse(rest)?;
292
293        Ok(Self {
294            op,
295            major: version.major,
296            minor: Some(version.minor),
297            patch: Some(version.patch),
298            pre: version.pre,
299        })
300    }
301
302    /// Parse a wildcard version like 1.*, 1.2.*, 1.x
303    fn parse_wildcard(text: &str) -> Result<Self, Error> {
304        let parts: Vec<&str> = text.split('.').collect();
305
306        let parse_part = |s: &str| -> Option<u64> {
307            if s == "*" || s == "x" || s == "X" {
308                None
309            } else {
310                s.parse().ok()
311            }
312        };
313
314        let major = parts
315            .first()
316            .and_then(|s| parse_part(s))
317            .ok_or_else(|| Error::InvalidVersion("invalid wildcard".to_string()))?;
318
319        let minor = parts.get(1).and_then(|s| parse_part(s));
320        let patch = parts.get(2).and_then(|s| parse_part(s));
321
322        Ok(Self {
323            op: Op::Wildcard,
324            major,
325            minor,
326            patch,
327            pre: crate::semver::Prerelease::EMPTY,
328        })
329    }
330
331    /// Check if a version matches this comparator
332    pub fn matches(&self, version: &Version) -> bool {
333        match self.op {
334            Op::Exact => self.matches_exact(version),
335            Op::Greater => self.matches_greater(version),
336            Op::GreaterEq => self.matches_greater_eq(version),
337            Op::Less => self.matches_less(version),
338            Op::LessEq => self.matches_less_eq(version),
339            Op::Caret => self.matches_caret(version),
340            Op::Tilde => self.matches_tilde(version),
341            Op::Wildcard => self.matches_wildcard(version),
342        }
343    }
344
345    fn matches_exact(&self, version: &Version) -> bool {
346        version.major == self.major
347            && self.minor.map_or(true, |m| version.minor == m)
348            && self.patch.map_or(true, |p| version.patch == p)
349            && (self.pre.is_empty() || version.pre == self.pre)
350    }
351
352    fn matches_greater(&self, version: &Version) -> bool {
353        let cmp_version = self.to_version();
354        version > &cmp_version
355    }
356
357    fn matches_greater_eq(&self, version: &Version) -> bool {
358        let cmp_version = self.to_version();
359        version >= &cmp_version
360    }
361
362    fn matches_less(&self, version: &Version) -> bool {
363        let cmp_version = self.to_version();
364        // For prereleases: 1.0.0-alpha < 1.0.0, but we shouldn't match
365        // prereleases unless the comparator also has a prerelease
366        if version.is_prerelease() && !cmp_version.is_prerelease() {
367            // Only match prereleases of the same major.minor.patch
368            if version.major == cmp_version.major
369                && version.minor == cmp_version.minor
370                && version.patch == cmp_version.patch
371            {
372                return true;
373            }
374            // Don't match prereleases of different versions
375            if version.base() >= cmp_version {
376                return false;
377            }
378        }
379        version < &cmp_version
380    }
381
382    fn matches_less_eq(&self, version: &Version) -> bool {
383        let cmp_version = self.to_version();
384        if version.is_prerelease() && !cmp_version.is_prerelease() {
385            if version.major == cmp_version.major
386                && version.minor == cmp_version.minor
387                && version.patch == cmp_version.patch
388            {
389                return true;
390            }
391            if version.base() > cmp_version {
392                return false;
393            }
394        }
395        version <= &cmp_version
396    }
397
398    /// Caret: ^1.2.3 means >=1.2.3 <2.0.0 (compatible with 1.x)
399    /// ^0.2.3 means >=0.2.3 <0.3.0 (for 0.x, minor is breaking)
400    /// ^0.0.3 means >=0.0.3 <0.0.4 (for 0.0.x, patch is breaking)
401    fn matches_caret(&self, version: &Version) -> bool {
402        // Must be >= the specified version
403        if !self.matches_greater_eq(version) {
404            return false;
405        }
406
407        // Determine the upper bound based on the first non-zero component
408        if self.major != 0 {
409            // ^1.2.3 => <2.0.0
410            version.major == self.major
411        } else if self.minor.unwrap_or(0) != 0 {
412            // ^0.2.3 => <0.3.0
413            version.major == 0 && version.minor == self.minor.unwrap_or(0)
414        } else {
415            // ^0.0.3 => <0.0.4
416            version.major == 0 && version.minor == 0 && version.patch == self.patch.unwrap_or(0)
417        }
418    }
419
420    /// Tilde: ~1.2.3 means >=1.2.3 <1.3.0 (patch-level changes only)
421    fn matches_tilde(&self, version: &Version) -> bool {
422        // Must be >= the specified version
423        if !self.matches_greater_eq(version) {
424            return false;
425        }
426
427        // Must have same major and minor
428        version.major == self.major && version.minor == self.minor.unwrap_or(0)
429    }
430
431    /// Wildcard: 1.* matches 1.0.0, 1.2.3, etc.
432    fn matches_wildcard(&self, version: &Version) -> bool {
433        if version.major != self.major {
434            return false;
435        }
436        if let Some(minor) = self.minor {
437            if version.minor != minor {
438                return false;
439            }
440        }
441        if let Some(patch) = self.patch {
442            if version.patch != patch {
443                return false;
444            }
445        }
446        true
447    }
448
449    /// Convert to a Version for comparison
450    fn to_version(&self) -> Version {
451        Version {
452            major: self.major,
453            minor: self.minor.unwrap_or(0),
454            patch: self.patch.unwrap_or(0),
455            pre: self.pre.clone(),
456            build: crate::semver::BuildMetadata::EMPTY,
457        }
458    }
459}
460
461impl fmt::Display for Comparator {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        write!(f, "{}", self.op)?;
464
465        if self.op == Op::Wildcard && self.minor.is_none() {
466            return write!(f, "{}.*", self.major);
467        }
468
469        write!(f, "{}", self.major)?;
470        if let Some(minor) = self.minor {
471            write!(f, ".{}", minor)?;
472            if let Some(patch) = self.patch {
473                write!(f, ".{}", patch)?;
474            } else if self.op == Op::Wildcard {
475                write!(f, ".*")?;
476            }
477        } else if self.op == Op::Wildcard {
478            write!(f, ".*")?;
479        }
480
481        if !self.pre.is_empty() {
482            write!(f, "-{}", self.pre)?;
483        }
484
485        Ok(())
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    #[test]
494    fn test_parse_exact() {
495        let req = VersionReq::parse("1.2.3").unwrap();
496        assert!(req.matches(&Version::new(1, 2, 3)));
497        assert!(!req.matches(&Version::new(1, 2, 4)));
498    }
499
500    #[test]
501    fn test_parse_comparison() {
502        let req = VersionReq::parse(">=1.0.0").unwrap();
503        assert!(req.matches(&Version::new(1, 0, 0)));
504        assert!(req.matches(&Version::new(2, 0, 0)));
505        assert!(!req.matches(&Version::new(0, 9, 0)));
506
507        let req = VersionReq::parse("<2.0.0").unwrap();
508        assert!(req.matches(&Version::new(1, 0, 0)));
509        assert!(!req.matches(&Version::new(2, 0, 0)));
510    }
511
512    #[test]
513    fn test_parse_combined() {
514        let req = VersionReq::parse(">=1.0.0 <2.0.0").unwrap();
515        assert!(req.matches(&Version::new(1, 0, 0)));
516        assert!(req.matches(&Version::new(1, 5, 0)));
517        assert!(!req.matches(&Version::new(0, 9, 0)));
518        assert!(!req.matches(&Version::new(2, 0, 0)));
519    }
520
521    #[test]
522    fn test_parse_caret() {
523        // ^1.2.3 := >=1.2.3 <2.0.0
524        let req = VersionReq::parse("^1.2.3").unwrap();
525        assert!(req.matches(&Version::new(1, 2, 3)));
526        assert!(req.matches(&Version::new(1, 9, 0)));
527        assert!(!req.matches(&Version::new(2, 0, 0)));
528        assert!(!req.matches(&Version::new(1, 2, 2)));
529
530        // ^0.2.3 := >=0.2.3 <0.3.0
531        let req = VersionReq::parse("^0.2.3").unwrap();
532        assert!(req.matches(&Version::new(0, 2, 3)));
533        assert!(req.matches(&Version::new(0, 2, 9)));
534        assert!(!req.matches(&Version::new(0, 3, 0)));
535
536        // ^0.0.3 := >=0.0.3 <0.0.4
537        let req = VersionReq::parse("^0.0.3").unwrap();
538        assert!(req.matches(&Version::new(0, 0, 3)));
539        assert!(!req.matches(&Version::new(0, 0, 4)));
540    }
541
542    #[test]
543    fn test_parse_tilde() {
544        // ~1.2.3 := >=1.2.3 <1.3.0
545        let req = VersionReq::parse("~1.2.3").unwrap();
546        assert!(req.matches(&Version::new(1, 2, 3)));
547        assert!(req.matches(&Version::new(1, 2, 9)));
548        assert!(!req.matches(&Version::new(1, 3, 0)));
549        assert!(!req.matches(&Version::new(1, 2, 2)));
550    }
551
552    #[test]
553    fn test_parse_wildcard() {
554        let req = VersionReq::parse("1.*").unwrap();
555        assert!(req.matches(&Version::new(1, 0, 0)));
556        assert!(req.matches(&Version::new(1, 9, 9)));
557        assert!(!req.matches(&Version::new(2, 0, 0)));
558
559        let req = VersionReq::parse("1.2.*").unwrap();
560        assert!(req.matches(&Version::new(1, 2, 0)));
561        assert!(req.matches(&Version::new(1, 2, 9)));
562        assert!(!req.matches(&Version::new(1, 3, 0)));
563    }
564
565    #[test]
566    fn test_parse_hyphen() {
567        let req = VersionReq::parse("1.0.0 - 2.0.0").unwrap();
568        assert!(req.matches(&Version::new(1, 0, 0)));
569        assert!(req.matches(&Version::new(1, 5, 0)));
570        assert!(req.matches(&Version::new(2, 0, 0)));
571        assert!(!req.matches(&Version::new(0, 9, 0)));
572        assert!(!req.matches(&Version::new(2, 0, 1)));
573    }
574
575    #[test]
576    fn test_parse_or() {
577        let req = VersionReq::parse("^1.0.0 || ^2.0.0").unwrap();
578        assert!(req.matches(&Version::new(1, 5, 0)));
579        assert!(req.matches(&Version::new(2, 5, 0)));
580        assert!(!req.matches(&Version::new(3, 0, 0)));
581    }
582
583    #[test]
584    fn test_star() {
585        let req = VersionReq::parse("*").unwrap();
586        assert!(req.matches(&Version::new(0, 0, 0)));
587        assert!(req.matches(&Version::new(999, 999, 999)));
588
589        let req = VersionReq::parse("").unwrap();
590        assert!(req.matches(&Version::new(1, 0, 0)));
591    }
592
593    #[test]
594    fn test_prerelease_matching() {
595        // Prereleases only match if explicitly specified or same base version
596        let req = VersionReq::parse(">=1.0.0-alpha").unwrap();
597        assert!(req.matches(&Version::parse("1.0.0-alpha").unwrap()));
598        assert!(req.matches(&Version::parse("1.0.0-beta").unwrap()));
599        assert!(req.matches(&Version::new(1, 0, 0)));
600    }
601
602    #[test]
603    fn test_display() {
604        assert_eq!(VersionReq::parse("^1.2.3").unwrap().to_string(), "^1.2.3");
605        assert_eq!(
606            VersionReq::parse(">=1.0.0 <2.0.0").unwrap().to_string(),
607            ">=1.0.0 <2.0.0"
608        );
609        assert_eq!(VersionReq::parse("*").unwrap().to_string(), "*");
610    }
611}