rust_toolchain/
version.rs

1use std::str::FromStr;
2
3/// A three component, `major.minor.patch` version number.
4///
5/// This version number is a subset of [semver](https://semver.org/spec/v2.0.0.html), except that
6/// it only accepts the numeric MAJOR, MINOR and PATCH components, while pre-release and build
7/// metadata, and other labels, are rejected.
8#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
9pub struct RustVersion {
10    version: version_number::FullVersion,
11}
12
13impl RustVersion {
14    /// Instantiate a semver compatible three component version number.
15    ///
16    /// This version is a subset of semver. It does not support the extensions
17    /// to the MAJOR.MINOR.PATCH format, i.e. the additional labels for
18    /// pre-releases and build metadata.
19    pub fn new(major: u64, minor: u64, patch: u64) -> Self {
20        Self {
21            version: version_number::FullVersion {
22                major,
23                minor,
24                patch,
25            },
26        }
27    }
28}
29
30impl RustVersion {
31    /// The major version of a semver three component version number
32    pub fn major(&self) -> u64 {
33        self.version.major
34    }
35
36    /// The minor version of a semver three component version number
37    pub fn minor(&self) -> u64 {
38        self.version.minor
39    }
40
41    /// The patch version of a semver three component version number
42    pub fn patch(&self) -> u64 {
43        self.version.patch
44    }
45}
46
47impl FromStr for RustVersion {
48    type Err = ParseError;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        use version_number::parsers::error::ExpectedError;
52        use version_number::parsers::error::NumericError;
53        use version_number::ParserError;
54
55        version_number::FullVersion::parse(s)
56            .map(|version| Self { version })
57            .map_err(|e| match e {
58                ParserError::Expected(inner) => match inner {
59                    ExpectedError::Numeric { got, .. } => ParseError::Expected("0-9", got),
60                    ExpectedError::Separator { got, .. } => ParseError::Expected(".", got),
61                    ExpectedError::EndOfInput { got, .. } => ParseError::Expected("EOI", Some(got)),
62                },
63                ParserError::Numeric(inner) => match inner {
64                    NumericError::LeadingZero => ParseError::LeadingZero,
65                    NumericError::Overflow => ParseError::NumberOverflow,
66                },
67            })
68    }
69}
70
71#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
72pub enum ParseError {
73    #[error("Expected '{0}' but got '{got}'", got = .1.map(|c| c.to_string()).unwrap_or_default())]
74    Expected(&'static str, Option<char>),
75
76    #[error("expected token 1-9, but got '0' (leading zero is not permitted)")]
77    LeadingZero,
78
79    #[error("unable to parse number (overflow occurred)")]
80    NumberOverflow,
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::cmp::Ordering;
87
88    #[test]
89    fn create_rust_version() {
90        let version = RustVersion::new(1, 2, 3);
91
92        assert_eq!(version.major(), 1);
93        assert_eq!(version.minor(), 2);
94        assert_eq!(version.patch(), 3);
95    }
96
97    #[test]
98    fn partial_eq() {
99        let left = RustVersion::new(1, 2, 3);
100        let right = RustVersion::new(1, 2, 3);
101
102        assert_eq!(left, right);
103    }
104
105    #[test]
106    fn eq() {
107        let left = RustVersion::new(1, 2, 3);
108        let right = RustVersion::new(1, 2, 3);
109
110        assert!(left.eq(&right));
111    }
112
113    #[yare::parameterized(
114        on_major = { RustVersion::new(1, 0, 0), RustVersion::new(0, 0, 0), Ordering::Greater },
115        on_minor = { RustVersion::new(1, 1, 0), RustVersion::new(1, 0, 0), Ordering::Greater },
116        on_patch = { RustVersion::new(1, 1, 1), RustVersion::new(1, 1, 0), Ordering::Greater },
117        eq = { RustVersion::new(1, 1, 1), RustVersion::new(1, 1, 1), Ordering::Equal },
118    )]
119    fn ordering(left: RustVersion, right: RustVersion, expected_ord: Ordering) {
120        assert_eq!(left.partial_cmp(&right), Some(expected_ord));
121        assert_eq!(left.cmp(&right), expected_ord);
122    }
123
124    mod partial_eq {
125        use super::*;
126
127        #[test]
128        fn symmetric() {
129            let left = RustVersion::new(1, 2, 3);
130            let right = RustVersion::new(1, 2, 3);
131
132            assert_eq!(
133                left, right,
134                "PartialEq should be symmetric: 'left == right' must hold"
135            );
136            assert_eq!(
137                right, left,
138                "PartialEq should be symmetric: 'right == left' must hold"
139            );
140        }
141
142        #[test]
143        fn transitive() {
144            let a = RustVersion::new(1, 2, 3);
145            let b = RustVersion::new(1, 2, 3);
146            let c = RustVersion::new(1, 2, 3);
147
148            assert_eq!(
149                a, b,
150                "PartialEq should be transitive: 'a == b' must hold, by symmetric property"
151            );
152            assert_eq!(
153                b, c,
154                "PartialEq should be transitive: 'b == c' must hold, by symmetric property"
155            );
156
157            assert_eq!(a, c, "PartialEq should be transitive: 'a == c' must hold, given a == b (prior) and b == c (prior)");
158        }
159    }
160
161    mod partial_ord {
162        use super::*;
163
164        #[test]
165        fn equality() {
166            let a = RustVersion::new(1, 2, 3);
167            let b = RustVersion::new(1, 2, 3);
168
169            assert_eq!(
170                a, b,
171                "PartialOrd should hold for equality: 'a == b' must hold"
172            );
173            assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal), "PartialOrd should hold for equality: 'a.partial_cmp(&b) == Ordering::Equal' must hold");
174        }
175
176        #[test]
177        fn transitive_lt() {
178            let a = RustVersion::new(1, 2, 1);
179            let b = RustVersion::new(1, 2, 2);
180            let c = RustVersion::new(1, 2, 3);
181
182            assert!(a < b, "PartialOrd should be transitive: 'a < b' must hold");
183            assert!(b < c, "PartialOrd should be transitive: 'b < c' must hold");
184            assert!(a < c, "PartialOrd should be transitive: 'a < c' must hold, given a < b (prior) and b < c (prior)");
185        }
186
187        #[test]
188        fn transitive_gt() {
189            let a = RustVersion::new(1, 2, 3);
190            let b = RustVersion::new(1, 2, 2);
191            let c = RustVersion::new(1, 2, 1);
192
193            assert!(a > b, "PartialOrd should be transitive: 'a > b' must hold");
194            assert!(b > c, "PartialOrd should be transitive: 'b > c' must hold");
195            assert!(a > c, "PartialOrd should be transitive: 'a > c' must hold, given a > b (prior) and b > c (prior)");
196        }
197    }
198}