1use pubgrub::Ranges;
7use semver::{Version, VersionReq};
8
9use crate::error::PkgError;
10
11pub fn parse_version_req(s: &str) -> Result<VersionReq, PkgError> {
16 let s = s.trim();
17 let req_str = if s.starts_with(|c: char| c.is_ascii_digit()) {
19 format!("^{s}")
20 } else {
21 s.to_string()
22 };
23 VersionReq::parse(&req_str).map_err(|e| PkgError::InvalidVersion(format!("{s}: {e}")))
24}
25
26pub fn parse_version(s: &str) -> Result<Version, PkgError> {
28 let normalized = if s.matches('.').count() < 2 {
30 format!("{s}.0")
31 } else {
32 s.to_string()
33 };
34 Version::parse(&normalized).map_err(|e| PkgError::InvalidVersion(format!("{s}: {e}")))
35}
36
37#[must_use]
41pub fn req_to_pubgrub_range(req: &VersionReq) -> Ranges<Version> {
42 let mut result = Ranges::full();
44
45 for comp in &req.comparators {
46 let range = comparator_to_range(comp);
47 result = result.intersection(&range);
48 }
49
50 result
51}
52
53fn comparator_to_range(comp: &semver::Comparator) -> Ranges<Version> {
54 let major = comp.major;
55 let minor = comp.minor.unwrap_or(0);
56 let patch = comp.patch.unwrap_or(0);
57 let version = Version::new(major, minor, patch);
58
59 match comp.op {
60 semver::Op::Exact => Ranges::singleton(version),
61 semver::Op::Greater => Ranges::strictly_higher_than(version),
62 semver::Op::GreaterEq => Ranges::higher_than(version),
63 semver::Op::Less => Ranges::strictly_lower_than(version),
64 semver::Op::LessEq => {
65 let next = Version::new(major, minor, patch + 1);
67 Ranges::strictly_lower_than(next)
68 }
69 semver::Op::Tilde => {
70 let upper = Version::new(major, minor + 1, 0);
72 Ranges::between(version, upper)
73 }
74 semver::Op::Caret => {
75 let upper = if major > 0 {
77 Version::new(major + 1, 0, 0)
78 } else if minor > 0 {
79 Version::new(0, minor + 1, 0)
80 } else {
81 Version::new(0, 0, patch + 1)
82 };
83 Ranges::between(version, upper)
84 }
85 semver::Op::Wildcard => {
86 if comp.minor.is_some() {
88 let lower = Version::new(major, minor, 0);
90 let upper = Version::new(major, minor + 1, 0);
91 Ranges::between(lower, upper)
92 } else {
93 let lower = Version::new(major, 0, 0);
95 let upper = Version::new(major + 1, 0, 0);
96 Ranges::between(lower, upper)
97 }
98 }
99 _ => Ranges::full(),
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn parse_caret_requirement() {
109 let req = parse_version_req("^1.0").unwrap();
110 assert!(req.matches(&Version::new(1, 5, 0)));
111 assert!(!req.matches(&Version::new(2, 0, 0)));
112 }
113
114 #[test]
115 fn parse_tilde_requirement() {
116 let req = parse_version_req("~1.2.3").unwrap();
117 assert!(req.matches(&Version::new(1, 2, 5)));
118 assert!(!req.matches(&Version::new(1, 3, 0)));
119 }
120
121 #[test]
122 fn parse_bare_version_as_caret() {
123 let req = parse_version_req("1.0.0").unwrap();
124 assert!(req.matches(&Version::new(1, 5, 0)));
125 assert!(!req.matches(&Version::new(2, 0, 0)));
126 }
127
128 #[test]
129 fn parse_exact_requirement() {
130 let req = parse_version_req("=1.2.3").unwrap();
131 assert!(req.matches(&Version::new(1, 2, 3)));
132 assert!(!req.matches(&Version::new(1, 2, 4)));
133 }
134
135 #[test]
136 fn caret_to_pubgrub_range() {
137 let req = parse_version_req("^1.0").unwrap();
138 let range = req_to_pubgrub_range(&req);
139 assert!(range.contains(&Version::new(1, 0, 0)));
140 assert!(range.contains(&Version::new(1, 9, 9)));
141 assert!(!range.contains(&Version::new(2, 0, 0)));
142 assert!(!range.contains(&Version::new(0, 9, 9)));
143 }
144
145 #[test]
146 fn tilde_to_pubgrub_range() {
147 let req = parse_version_req("~1.2.0").unwrap();
148 let range = req_to_pubgrub_range(&req);
149 assert!(range.contains(&Version::new(1, 2, 0)));
150 assert!(range.contains(&Version::new(1, 2, 9)));
151 assert!(!range.contains(&Version::new(1, 3, 0)));
152 }
153
154 #[test]
155 fn parse_two_component_version() {
156 let v = parse_version("1.0").unwrap();
157 assert_eq!(v, Version::new(1, 0, 0));
158 }
159}