deps_core/
version_matcher.rs1use semver::Version;
7
8pub trait VersionRequirementMatcher: Send + Sync {
13 fn is_latest_satisfying(&self, requirement: &str, latest: &str) -> bool;
27}
28
29#[derive(Debug, Clone, Copy)]
34pub struct SemverMatcher;
35
36impl VersionRequirementMatcher for SemverMatcher {
37 fn is_latest_satisfying(&self, requirement: &str, latest: &str) -> bool {
38 use semver::VersionReq;
39
40 let latest_ver = match latest.parse::<Version>() {
42 Ok(v) => v,
43 Err(_) => return requirement == latest,
44 };
45
46 if let Ok(req) = requirement.parse::<VersionReq>() {
48 return req.matches(&latest_ver);
49 }
50
51 if let Ok(req) = format!("^{}", requirement).parse::<VersionReq>() {
54 return req.matches(&latest_ver);
55 }
56
57 requirement == latest
59 }
60}
61
62#[derive(Debug, Clone, Copy)]
71pub struct Pep440Matcher;
72
73impl VersionRequirementMatcher for Pep440Matcher {
74 fn is_latest_satisfying(&self, requirement: &str, latest: &str) -> bool {
75 let latest_ver = match normalize_and_parse_version(latest) {
77 Some(v) => v,
78 None => return requirement == latest,
79 };
80
81 let min_version = extract_pypi_min_version(requirement);
84
85 let min_ver = match min_version.and_then(|v| normalize_and_parse_version(&v)) {
86 Some(v) => v,
87 None => return requirement == latest,
88 };
89
90 if min_ver.major == 0 {
92 min_ver.major == latest_ver.major && min_ver.minor == latest_ver.minor
94 } else {
95 min_ver.major == latest_ver.major
97 }
98 }
99}
100
101pub fn normalize_and_parse_version(version: &str) -> Option<Version> {
114 if let Ok(v) = version.parse::<Version>() {
116 return Some(v);
117 }
118
119 let dot_count = version.chars().filter(|&c| c == '.').count();
121
122 let normalized = match dot_count {
123 0 => format!("{}.0.0", version), 1 => format!("{}.0", version), _ => version.to_string(),
126 };
127
128 normalized.parse::<Version>().ok()
129}
130
131pub fn extract_pypi_min_version(version_req: &str) -> Option<String> {
143 for part in version_req.split(',') {
145 let trimmed = part.trim();
146
147 if let Some(ver) = trimmed.strip_prefix(">=") {
149 return Some(ver.trim().to_string());
150 }
151 if let Some(ver) = trimmed.strip_prefix("~=") {
152 return Some(ver.trim().to_string());
153 }
154 if let Some(ver) = trimmed.strip_prefix("==") {
155 return Some(ver.trim().to_string());
156 }
157 if let Some(ver) = trimmed.strip_prefix('>') {
158 return Some(ver.trim().to_string());
160 }
161 }
162
163 let stripped = version_req.trim_start_matches('^').trim_start_matches('~');
166 if stripped.chars().next().is_some_and(|c| c.is_ascii_digit()) {
167 return Some(stripped.to_string());
168 }
169
170 None
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_semver_matcher_exact_match() {
179 let matcher = SemverMatcher;
180 assert!(matcher.is_latest_satisfying("1.0.0", "1.0.0"));
181 assert!(matcher.is_latest_satisfying("^1.0.0", "1.0.0"));
182 assert!(matcher.is_latest_satisfying("~1.0.0", "1.0.0"));
183 assert!(matcher.is_latest_satisfying("=1.0.0", "1.0.0"));
184 }
185
186 #[test]
187 fn test_semver_matcher_compatible_versions() {
188 let matcher = SemverMatcher;
189 assert!(matcher.is_latest_satisfying("1.0.0", "1.0.5")); assert!(matcher.is_latest_satisfying("^1.0.0", "1.5.0")); assert!(matcher.is_latest_satisfying("0.1", "0.1.83")); assert!(matcher.is_latest_satisfying("1", "1.5.0")); }
195
196 #[test]
197 fn test_semver_matcher_incompatible_versions() {
198 let matcher = SemverMatcher;
199 assert!(!matcher.is_latest_satisfying("1.0.0", "2.0.0")); assert!(!matcher.is_latest_satisfying("0.1", "0.2.0")); assert!(!matcher.is_latest_satisfying("~1.0.0", "1.1.0")); }
204
205 #[test]
206 fn test_pep440_matcher_same_major() {
207 let matcher = Pep440Matcher;
208 assert!(matcher.is_latest_satisfying(">=8.0", "8.3.5")); assert!(matcher.is_latest_satisfying(">=1.0", "1.5.0")); assert!(matcher.is_latest_satisfying(">=1.0,<2.0", "1.9.0")); }
213
214 #[test]
215 fn test_pep440_matcher_new_major() {
216 let matcher = Pep440Matcher;
217 assert!(!matcher.is_latest_satisfying(">=8.0", "9.0.2")); assert!(!matcher.is_latest_satisfying(">=1.0", "2.0.0")); assert!(!matcher.is_latest_satisfying(">=4.0,<8.0", "8.0.0")); }
222
223 #[test]
224 fn test_pep440_matcher_zero_version() {
225 let matcher = Pep440Matcher;
226 assert!(matcher.is_latest_satisfying(">=0.8", "0.8.5")); assert!(!matcher.is_latest_satisfying(">=0.8", "0.9.0")); }
230
231 #[test]
232 fn test_extract_pypi_min_version() {
233 assert_eq!(extract_pypi_min_version(">=8.0"), Some("8.0".to_string()));
234 assert_eq!(
235 extract_pypi_min_version(">=1.0,<2.0"),
236 Some("1.0".to_string())
237 );
238 assert_eq!(
239 extract_pypi_min_version("~=1.4.2"),
240 Some("1.4.2".to_string())
241 );
242 assert_eq!(
243 extract_pypi_min_version("==2.0.0"),
244 Some("2.0.0".to_string())
245 );
246 assert_eq!(extract_pypi_min_version("^1.0"), Some("1.0".to_string())); assert_eq!(extract_pypi_min_version(">1.0"), Some("1.0".to_string()));
248 }
249
250 #[test]
251 fn test_normalize_and_parse_version() {
252 assert_eq!(
253 normalize_and_parse_version("1.0.0").unwrap().to_string(),
254 "1.0.0"
255 );
256 assert_eq!(
257 normalize_and_parse_version("1.0").unwrap().to_string(),
258 "1.0.0"
259 );
260 assert_eq!(
261 normalize_and_parse_version("8").unwrap().to_string(),
262 "8.0.0"
263 );
264 assert!(normalize_and_parse_version("invalid").is_none());
265 }
266}