1mod part;
2
3use alloc::{borrow::Cow, string::String};
4use core::{
5 cmp::{Ordering, Reverse},
6 convert::Infallible,
7 fmt,
8 hash::{Hash, Hasher},
9 str::FromStr,
10};
11
12use compact_str::CompactString;
13use itertools::{EitherOrBoth, Itertools};
14use part::VersionPart;
15use smallvec::SmallVec;
16
17#[derive(Clone, Debug, Default, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
19#[cfg_attr(feature = "serde", serde(from = "&str"))]
20pub struct Version {
21 raw: CompactString,
23 parts: SmallVec<[VersionPart; 6]>,
25}
26
27impl Version {
28 const SEPARATOR: char = '.';
29
30 pub fn new<T: AsRef<str>>(input: T) -> Self {
31 let mut trimmed = input.as_ref().trim();
32
33 if let Some(digit_pos) = trimmed.find(|char: char| char.is_ascii_digit()) {
36 if trimmed
37 .find('.')
38 .is_none_or(|separator_pos| digit_pos < separator_pos)
39 {
40 trimmed = &trimmed[digit_pos..];
41 }
42 }
43
44 let mut parts = trimmed
46 .split(Self::SEPARATOR)
47 .map(VersionPart::from)
48 .collect::<SmallVec<[_; 6]>>();
49
50 if let Some(pos) = parts.iter().rposition(|part| !part.is_droppable()) {
52 parts.truncate(pos + 1);
53 } else {
54 parts.clear();
55 }
56
57 Self {
58 raw: CompactString::from(trimmed),
59 parts,
60 }
61 }
62
63 #[must_use]
79 #[inline]
80 pub fn is_latest(&self) -> bool {
81 const LATEST: &str = "latest";
82
83 self.raw.eq_ignore_ascii_case(LATEST)
84 }
85
86 #[must_use]
102 #[inline]
103 pub fn is_unknown(&self) -> bool {
104 const UNKNOWN: &str = "unknown";
105
106 self.raw.eq_ignore_ascii_case(UNKNOWN)
107 }
108
109 #[must_use]
111 #[inline]
112 pub fn as_str(&self) -> &str {
113 self.raw.as_str()
114 }
115
116 pub fn closest<'iter, I, T>(&self, versions: I) -> Option<&'iter T>
130 where
131 I: IntoIterator<Item = &'iter T>,
132 &'iter T: Into<&'iter Self>,
133 {
134 #[derive(PartialEq, Eq, PartialOrd, Ord)]
135 struct DistanceKey<'supplement> {
136 length_score: usize,
138 numerical_difference: u64,
140 total_order: Ordering,
142 supplement_order: Reverse<&'supplement str>,
144 }
145
146 let default_part = &VersionPart::DEFAULT;
147
148 versions.into_iter().min_by_key(|&other| {
150 self.parts
151 .iter()
152 .zip_longest(other.into().parts.iter())
153 .map(|pair| match pair {
154 EitherOrBoth::Both(part, other_part) => (part, other_part),
155 EitherOrBoth::Left(part) => (part, default_part),
156 EitherOrBoth::Right(other_part) => (default_part, other_part),
157 })
158 .enumerate()
159 .find_map(|(index, (part, other_part))| {
160 (part != other_part).then(|| DistanceKey {
161 length_score: !index,
162 numerical_difference: part.number.abs_diff(other_part.number),
163 total_order: part.cmp(other_part),
164 supplement_order: Reverse(other_part.supplement.as_str()),
165 })
166 })
167 .unwrap_or(DistanceKey {
168 length_score: 0,
169 numerical_difference: 0,
170 total_order: Ordering::Equal,
171 supplement_order: Reverse(""),
172 })
173 })
174 }
175}
176
177impl AsRef<str> for Version {
178 #[inline]
179 fn as_ref(&self) -> &str {
180 self.as_str()
181 }
182}
183
184impl fmt::Display for Version {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 self.raw.fmt(f)
187 }
188}
189
190impl FromStr for Version {
191 type Err = Infallible;
192
193 fn from_str(s: &str) -> Result<Self, Self::Err> {
194 Ok(Self::new(s))
195 }
196}
197
198impl From<&str> for Version {
199 #[inline]
200 fn from(s: &str) -> Self {
201 Self::new(s)
202 }
203}
204
205impl From<String> for Version {
206 #[inline]
207 fn from(s: String) -> Self {
208 Self::new(s)
209 }
210}
211
212impl From<&String> for Version {
213 #[inline]
214 fn from(s: &String) -> Self {
215 Self::new(s)
216 }
217}
218
219impl From<Cow<'_, str>> for Version {
220 #[inline]
221 fn from(s: Cow<'_, str>) -> Self {
222 Self::new(s)
223 }
224}
225
226impl PartialEq for Version {
227 fn eq(&self, other: &Self) -> bool {
228 (self.is_latest() && other.is_latest())
229 || (self.is_unknown() && other.is_unknown())
230 || self.parts.eq(&other.parts)
231 }
232}
233
234impl Hash for Version {
235 fn hash<H: Hasher>(&self, state: &mut H) {
236 self.parts.hash(state);
237 }
238}
239
240impl PartialOrd for Version {
241 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
242 Some(self.cmp(other))
243 }
244}
245
246impl Ord for Version {
247 fn cmp(&self, other: &Self) -> Ordering {
248 match (self.is_latest(), other.is_latest()) {
249 (true, true) => Ordering::Equal,
250 (true, false) => Ordering::Greater,
251 (false, true) => Ordering::Less,
252 (false, false) => match (self.is_unknown(), other.is_unknown()) {
253 (true, true) => Ordering::Equal,
254 (true, false) => Ordering::Less,
255 (false, true) => Ordering::Greater,
256 (false, false) => self
257 .parts
258 .iter()
259 .zip_longest(&other.parts)
260 .map(|pair| match pair {
261 EitherOrBoth::Both(part, other_part) => part.cmp(other_part),
262 EitherOrBoth::Left(part) => part.cmp(&VersionPart::DEFAULT),
263 EitherOrBoth::Right(other_part) => VersionPart::DEFAULT.cmp(other_part),
264 })
265 .find(|&ordering| ordering != Ordering::Equal)
266 .unwrap_or(Ordering::Equal),
267 },
268 }
269 }
270}
271
272#[cfg(feature = "serde")]
273impl serde::Serialize for Version {
274 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
275 where
276 S: serde::Serializer,
277 {
278 self.as_str().serialize(serializer)
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use alloc::vec::Vec;
285 use core::cmp::Ordering;
286
287 use rstest::rstest;
288
289 use super::Version;
290
291 #[rstest]
292 #[case("1.0", "1.0.0")]
293 #[case("1.2.00.3", "1.2.0.3")]
294 #[case("1.2.003.4", "1.2.3.4")]
295 #[case("01.02.03.04", "1.2.3.4")]
296 #[case("1.2.03-beta", "1.2.3-beta")]
297 #[case("1.0", "1.0 ")]
298 #[case("1.0", "1. 0")]
299 #[case("1.0", "1.0.")]
300 #[case("1.0", "Version 1.0")]
301 #[case("2.4.2", "v2.4.2")]
302 #[case("foo1", "bar1")]
303 #[case("latest", "LATEST")]
304 #[case("unknown", "UNKNOWN")]
305 fn version_equality(#[case] left: &str, #[case] right: &str) {
306 let left = Version::new(left);
307 let right = Version::new(right);
308 assert_eq!(left, right);
309 assert_eq!(left.cmp(&right), Ordering::Equal);
310 }
311
312 #[rstest]
313 #[case("1", "2")]
314 #[case("1.2-rc", "1.2")]
315 #[case("1.0-rc", "1.0")]
316 #[case("1.0.0-rc", "1")]
317 #[case("22.0.0-rc.1", "22.0.0")]
318 #[case("22.0.0-rc.1", "22.0.0.1")]
319 #[case("22.0.0-rc.1", "22.0.0.1-rc")]
320 #[case("22.0.0-rc.1", "22.0.0-rc.1.1")]
321 #[case("22.0.0-rc.1.1", "22.0.0-rc.1.2")]
322 #[case("22.0.0-rc.1.2", "22.0.0-rc.2")]
323 #[case("v0.0.1", "0.0.2")]
324 #[case("v0.0.1", "v0.0.2")]
325 #[case("1.a2", "1.b1")]
326 #[case("alpha", "beta")]
327 #[case("99999.99999.99999", "latest")]
328 #[case("unknown", "1.2.3")]
329 #[case("unknown", "latest")]
330 fn version_comparison_and_inequality(#[case] left: Version, #[case] right: Version) {
331 assert!(left < right);
332 assert!(right > left);
333 assert_ne!(left, right)
334 }
335
336 #[rstest]
337 #[case("1", "2")]
338 #[case("1-rc", "1")]
339 #[case("1-a2", "1-b1")]
340 #[case("alpha", "beta")]
341 fn version_part_comparison(#[case] left: Version, #[case] right: Version) {
342 assert!(left < right);
343 assert!(right > left);
344 }
345
346 #[test]
347 fn version_hash() {
348 use core::hash::BuildHasher;
349
350 use rustc_hash::FxBuildHasher;
351
352 let version1 = Version::new("1.2.3");
356 let version2 = Version::new("1.2.3.0");
357 assert_eq!(version1, version2);
358
359 assert_eq!(
360 FxBuildHasher.hash_one(version1),
361 FxBuildHasher.hash_one(version2)
362 );
363 }
364
365 #[test]
366 fn only_supplement() {
367 const ALPHA: &str = "alpha";
368
369 let version = Version::new(ALPHA);
370 assert_eq!(version.parts.len(), 1);
371 assert_eq!(version.parts[0].number, 0);
372 assert_eq!(version.parts[0].supplement, ALPHA);
373 }
374
375 #[rstest]
376 #[case("0")]
377 #[case("0.0.0")]
378 #[case("0.0.0.0.0.0.0.0")]
379 #[case("")]
380 fn only_droppable_parts(#[case] version: Version) {
381 assert_eq!(version.parts.len(), 0);
382 }
383
384 #[rstest]
385 #[case("1.2.3", &["1.0.0", "0.9.0", "1.5.6.3", "1.3.2"], "1.3.2")]
386 #[case("10.20.30", &["10.20.29", "10.20.31", "10.20.40"], "10.20.31")]
387 #[case("5.5.5", &["5.5.50", "5.5.0", "5.5.10"], "5.5.10")]
388 #[case("3.0.0", &["3.0.0-beta", "3.0.0-alpha.1", "3.0.0-rc.1"], "3.0.0-rc.1")]
389 #[case("2.1.0-beta", &["2.1.0-alpha", "2.1.0-beta.2", "2.1.0"], "2.1.0-beta.2")]
390 #[case("1.5.0", &["1.0.0", "2.0.0"], "1.0.0")]
391 #[case("3.3.3", &["1.1.1", "5.5.5"], "5.5.5")]
392 #[case("3.3.3", &["5.5.5", "1.1.1"], "5.5.5")]
393 #[case("2.2.2", &["2.2.2", "2.2.2", "2.2.3"], "2.2.2")]
394 #[case("0.0.2", &["0.0.1", "0.0.3", "0.2.0"], "0.0.3")]
395 #[case("999.999.999", &["999.999.998", "1000.0.0"], "999.999.998")]
396 fn closest_version(#[case] version: &str, #[case] versions: &[&str], #[case] expected: &str) {
397 let versions = versions.into_iter().map(Version::new).collect::<Vec<_>>();
398 assert_eq!(
399 Version::new(version).closest(&versions),
400 Some(&Version::new(expected))
401 );
402 }
403}