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 FromStr for RustVersion {
50    type Err = ParseError;
51
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        use version_number::parsers::error::ExpectedError;
54        use version_number::parsers::error::NumericError;
55        use version_number::ParserError;
56
57        version_number::FullVersion::parse(s)
58            .map(|version| Self { version })
59            .map_err(|e| match e {
60                ParserError::Expected(inner) => match inner {
61                    ExpectedError::Numeric { got, .. } => ParseError::Expected("0-9", got),
62                    ExpectedError::Separator { got, .. } => ParseError::Expected(".", got),
63                    ExpectedError::EndOfInput { got, .. } => ParseError::Expected("EOI", Some(got)),
64                },
65                ParserError::Numeric(inner) => match inner {
66                    NumericError::LeadingZero => ParseError::LeadingZero,
67                    NumericError::Overflow => ParseError::NumberOverflow,
68                },
69            })
70    }
71}
72
73impl fmt::Display for RustVersion {
74    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
75        write!(f, "{}", self.version)
76    }
77}
78
79#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
80pub enum ParseError {
81    #[error("Expected '{0}' but got '{got}'", got = .1.map(|c| c.to_string()).unwrap_or_default())]
82    Expected(&'static str, Option<char>),
83
84    #[error("expected token 1-9, but got '0' (leading zero is not permitted)")]
85    LeadingZero,
86
87    #[error("unable to parse number (overflow occurred)")]
88    NumberOverflow,
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use std::cmp::Ordering;
95
96    #[test]
97    fn create_rust_version() {
98        let version = RustVersion::new(1, 2, 3);
99
100        assert_eq!(version.major(), 1);
101        assert_eq!(version.minor(), 2);
102        assert_eq!(version.patch(), 3);
103    }
104
105    #[test]
106    fn display() {
107        let version = RustVersion::new(12, 2, 24);
108
109        assert_eq!(&format!("{version}"), "12.2.24");
110    }
111
112    #[test]
113    fn partial_eq() {
114        let left = RustVersion::new(1, 2, 3);
115        let right = RustVersion::new(1, 2, 3);
116
117        assert_eq!(left, right);
118    }
119
120    #[test]
121    fn eq() {
122        let left = RustVersion::new(1, 2, 3);
123        let right = RustVersion::new(1, 2, 3);
124
125        assert!(left.eq(&right));
126    }
127
128    #[yare::parameterized(
129        on_major = { RustVersion::new(1, 0, 0), RustVersion::new(0, 0, 0), Ordering::Greater },
130        on_minor = { RustVersion::new(1, 1, 0), RustVersion::new(1, 0, 0), Ordering::Greater },
131        on_patch = { RustVersion::new(1, 1, 1), RustVersion::new(1, 1, 0), Ordering::Greater },
132        eq = { RustVersion::new(1, 1, 1), RustVersion::new(1, 1, 1), Ordering::Equal },
133    )]
134    fn ordering(left: RustVersion, right: RustVersion, expected_ord: Ordering) {
135        assert_eq!(left.partial_cmp(&right), Some(expected_ord));
136        assert_eq!(left.cmp(&right), expected_ord);
137    }
138
139    mod partial_eq {
140        use super::*;
141
142        #[test]
143        fn symmetric() {
144            let left = RustVersion::new(1, 2, 3);
145            let right = RustVersion::new(1, 2, 3);
146
147            assert_eq!(
148                left, right,
149                "PartialEq should be symmetric: 'left == right' must hold"
150            );
151            assert_eq!(
152                right, left,
153                "PartialEq should be symmetric: 'right == left' must hold"
154            );
155        }
156
157        #[test]
158        fn transitive() {
159            let a = RustVersion::new(1, 2, 3);
160            let b = RustVersion::new(1, 2, 3);
161            let c = RustVersion::new(1, 2, 3);
162
163            assert_eq!(
164                a, b,
165                "PartialEq should be transitive: 'a == b' must hold, by symmetric property"
166            );
167            assert_eq!(
168                b, c,
169                "PartialEq should be transitive: 'b == c' must hold, by symmetric property"
170            );
171
172            assert_eq!(a, c, "PartialEq should be transitive: 'a == c' must hold, given a == b (prior) and b == c (prior)");
173        }
174    }
175
176    mod partial_ord {
177        use super::*;
178
179        #[test]
180        fn equality() {
181            let a = RustVersion::new(1, 2, 3);
182            let b = RustVersion::new(1, 2, 3);
183
184            assert_eq!(
185                a, b,
186                "PartialOrd should hold for equality: 'a == b' must hold"
187            );
188            assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal), "PartialOrd should hold for equality: 'a.partial_cmp(&b) == Ordering::Equal' must hold");
189        }
190
191        #[test]
192        fn transitive_lt() {
193            let a = RustVersion::new(1, 2, 1);
194            let b = RustVersion::new(1, 2, 2);
195            let c = RustVersion::new(1, 2, 3);
196
197            assert!(a < b, "PartialOrd should be transitive: 'a < b' must hold");
198            assert!(b < c, "PartialOrd should be transitive: 'b < c' must hold");
199            assert!(a < c, "PartialOrd should be transitive: 'a < c' must hold, given a < b (prior) and b < c (prior)");
200        }
201
202        #[test]
203        fn transitive_gt() {
204            let a = RustVersion::new(1, 2, 3);
205            let b = RustVersion::new(1, 2, 2);
206            let c = RustVersion::new(1, 2, 1);
207
208            assert!(a > b, "PartialOrd should be transitive: 'a > b' must hold");
209            assert!(b > c, "PartialOrd should be transitive: 'b > c' must hold");
210            assert!(a > c, "PartialOrd should be transitive: 'a > c' must hold, given a > b (prior) and b > c (prior)");
211        }
212    }
213}