Skip to main content

headless_lms_utils/
version.rs

1//! Release versions that clients report, for rejecting or warning about outdated ones.
2
3use std::fmt;
4
5/// A `MAJOR.MINOR.PATCH` version, ordered by its numbers.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub struct Version(pub u64, pub u64, pub u64);
8
9impl Version {
10    /// Parses `MAJOR[.MINOR[.PATCH]]`, missing parts being 0, with an optional leading `v`. Parts
11    /// are digits only, so a pre-release or other suffix is no version.
12    pub fn parse(raw: &str) -> Option<Self> {
13        let parts: Vec<u64> = raw
14            .trim()
15            .trim_start_matches('v')
16            .split('.')
17            .map(|part| {
18                (!part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()))
19                    .then(|| part.parse().ok())
20                    .flatten()
21            })
22            .collect::<Option<_>>()?;
23        match parts.as_slice() {
24            [major] => Some(Version(*major, 0, 0)),
25            [major, minor] => Some(Version(*major, *minor, 0)),
26            [major, minor, patch] => Some(Version(*major, *minor, *patch)),
27            _ => None,
28        }
29    }
30}
31
32impl fmt::Display for Version {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(f, "{}.{}.{}", self.0, self.1, self.2)
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn missing_parts_default_to_zero() {
44        assert_eq!(Version::parse("1"), Some(Version(1, 0, 0)));
45        assert_eq!(Version::parse("1.2"), Some(Version(1, 2, 0)));
46        assert_eq!(Version::parse("1.2.3"), Some(Version(1, 2, 3)));
47        assert_eq!(Version::parse("x"), None);
48    }
49}