dependency_check_updates_core/
version.rs1use crate::types::TargetLevel;
10
11pub trait SelectableVersion: std::fmt::Display {
18 fn is_prerelease(&self) -> bool;
20 fn major(&self) -> u64;
22 fn minor(&self) -> u64;
24 fn patch(&self) -> u64;
26}
27
28impl SelectableVersion for node_semver::Version {
29 fn is_prerelease(&self) -> bool {
30 !self.pre_release.is_empty()
31 }
32 fn major(&self) -> u64 {
33 self.major
34 }
35 fn minor(&self) -> u64 {
36 self.minor
37 }
38 fn patch(&self) -> u64 {
39 self.patch
40 }
41}
42
43impl SelectableVersion for semver::Version {
44 fn is_prerelease(&self) -> bool {
45 !self.pre.is_empty()
46 }
47 fn major(&self) -> u64 {
48 self.major
49 }
50 fn minor(&self) -> u64 {
51 self.minor
52 }
53 fn patch(&self) -> u64 {
54 self.patch
55 }
56}
57
58impl SelectableVersion for pep440_rs::Version {
59 fn is_prerelease(&self) -> bool {
62 self.any_prerelease()
63 }
64 fn major(&self) -> u64 {
65 self.release().first().copied().unwrap_or(0)
66 }
67 fn minor(&self) -> u64 {
68 self.release().get(1).copied().unwrap_or(0)
69 }
70 fn patch(&self) -> u64 {
71 self.release().get(2).copied().unwrap_or(0)
72 }
73}
74
75#[must_use]
91pub fn select_version<V: SelectableVersion>(
92 current: Option<&V>,
93 all_versions: &[V],
94 target: TargetLevel,
95 latest_for_stable: Option<String>,
96 unparseable_minor_patch: Option<String>,
97) -> Option<String> {
98 if all_versions.is_empty() {
99 return latest_for_stable;
100 }
101
102 let current_is_prerelease = current.is_some_and(SelectableVersion::is_prerelease);
103
104 let accept = |v: &&V| -> bool {
108 if !v.is_prerelease() {
109 return true;
110 }
111 current.is_some_and(|cur| {
112 cur.is_prerelease()
113 && v.major() == cur.major()
114 && v.minor() == cur.minor()
115 && v.patch() == cur.patch()
116 })
117 };
118
119 match target {
123 TargetLevel::Latest if current_is_prerelease => all_versions
124 .iter()
125 .rev()
126 .find(accept)
127 .map(ToString::to_string),
128 TargetLevel::Latest => latest_for_stable,
129 TargetLevel::Greatest | TargetLevel::Newest => all_versions.last().map(ToString::to_string),
130 TargetLevel::Minor => match current {
131 None => unparseable_minor_patch,
132 Some(cur) => all_versions
133 .iter()
134 .rev()
135 .find(|v| v.major() == cur.major() && accept(v))
136 .map(ToString::to_string),
137 },
138 TargetLevel::Patch => match current {
139 None => unparseable_minor_patch,
140 Some(cur) => all_versions
141 .iter()
142 .rev()
143 .find(|v| v.major() == cur.major() && v.minor() == cur.minor() && accept(v))
144 .map(ToString::to_string),
145 },
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use rstest::rstest;
153
154 fn vers(specs: &[&str]) -> Vec<semver::Version> {
155 let mut v: Vec<_> = specs
156 .iter()
157 .filter_map(|s| semver::Version::parse(s).ok())
158 .collect();
159 v.sort();
160 v
161 }
162
163 fn parse(s: &str) -> Option<semver::Version> {
164 semver::Version::parse(s).ok()
165 }
166
167 #[rstest]
176 #[case::empty_returns_latest_for_stable(
177 None,
178 &[],
179 TargetLevel::Greatest,
180 Some("1.0.0"),
181 None,
182 Some("1.0.0"),
183 )]
184 #[case::latest_stable_returns_fallback(
185 Some("1.0.0"),
186 &["1.0.0", "1.5.0", "2.0.0"],
187 TargetLevel::Latest,
188 Some("2.0.0"),
189 None,
190 Some("2.0.0"),
191 )]
192 #[case::minor_stays_on_major(
193 Some("1.0.0"),
194 &["1.0.0", "1.5.0", "2.0.0"],
195 TargetLevel::Minor,
196 None,
197 None,
198 Some("1.5.0"),
199 )]
200 #[case::patch_stays_on_minor(
201 Some("1.0.0"),
202 &["1.0.0", "1.0.5", "1.1.0", "2.0.0"],
203 TargetLevel::Patch,
204 None,
205 None,
206 Some("1.0.5"),
207 )]
208 #[case::greatest_includes_prerelease(
209 Some("1.0.0"),
210 &["1.0.0", "2.0.0-rc.1"],
211 TargetLevel::Greatest,
212 None,
213 None,
214 Some("2.0.0-rc.1"),
215 )]
216 #[case::latest_stable_excludes_prerelease_via_fallback(
218 Some("1.0.0"),
219 &["1.0.0", "2.0.0-rc.1"],
220 TargetLevel::Latest,
221 Some("1.0.0"),
222 None,
223 Some("1.0.0"),
224 )]
225 #[case::prerelease_tail_same_train(
227 Some("2.0.0-rc.1"),
228 &["1.1.0", "2.0.0-rc.1", "2.0.0-rc.2"],
229 TargetLevel::Latest,
230 Some("1.1.0"),
231 None,
232 Some("2.0.0-rc.2"),
233 )]
234 #[case::unparseable_minor_uses_dedicated_fallback(
236 None,
237 &["1.0.0", "2.0.0"],
238 TargetLevel::Minor,
239 Some("2.0.0"),
240 None,
241 None,
242 )]
243 #[case::unparseable_patch_uses_dedicated_fallback(
246 None,
247 &["1.0.0", "1.0.1", "2.0.0"],
248 TargetLevel::Patch,
249 Some("2.0.0"),
250 None,
251 None,
252 )]
253 #[case::newest_returns_last_candidate(
256 Some("1.0.0"),
257 &["1.0.0", "1.5.0", "2.0.0"],
258 TargetLevel::Newest,
259 None,
260 None,
261 Some("2.0.0"),
262 )]
263 fn select_version_cases(
264 #[case] current_str: Option<&str>,
265 #[case] version_strs: &[&str],
266 #[case] target: TargetLevel,
267 #[case] latest_for_stable: Option<&str>,
268 #[case] unparseable_minor_patch: Option<&str>,
269 #[case] expected: Option<&str>,
270 ) {
271 let cur = current_str.and_then(parse);
272 let candidates = vers(version_strs);
273 let selected = select_version(
274 cur.as_ref(),
275 &candidates,
276 target,
277 latest_for_stable.map(ToOwned::to_owned),
278 unparseable_minor_patch.map(ToOwned::to_owned),
279 );
280 assert_eq!(selected, expected.map(ToOwned::to_owned));
281 }
282
283 #[test]
284 fn test_selectable_version_trait_accessors() {
285 let v = semver::Version::parse("3.4.5-beta.1").unwrap();
286 assert!(v.is_prerelease());
287 assert_eq!(v.major(), 3);
288 assert_eq!(v.minor(), 4);
289 assert_eq!(v.patch(), 5);
290
291 let nv = node_semver::Version::parse("6.7.8").unwrap();
292 assert!(!nv.is_prerelease());
293 assert_eq!(nv.major(), 6);
294 assert_eq!(nv.minor(), 7);
295 assert_eq!(nv.patch(), 8);
296
297 let pv: pep440_rs::Version = "9.10.11".parse().unwrap();
298 assert!(!pv.is_prerelease());
299 assert_eq!(pv.major(), 9);
300 assert_eq!(pv.minor(), 10);
301 assert_eq!(pv.patch(), 11);
302
303 let pre: pep440_rs::Version = "2.0a1".parse().unwrap();
305 assert!(pre.is_prerelease());
306 assert_eq!(pre.major(), 2);
307 assert_eq!(pre.minor(), 0);
308 let dev: pep440_rs::Version = "1.0.dev0".parse().unwrap();
309 assert!(dev.is_prerelease());
310 let short: pep440_rs::Version = "5".parse().unwrap();
311 assert_eq!(short.major(), 5);
312 assert_eq!(short.minor(), 0);
313 assert_eq!(short.patch(), 0);
314 }
315}