Skip to main content

rust_toolchain/
version.rs

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