1use std::fmt;
2use std::fmt::Formatter;
3use std::str::FromStr;
4
5#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
11pub struct RustVersion {
12 version: version_number::FullVersion,
13}
14
15impl RustVersion {
16 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 pub fn major(&self) -> u64 {
35 self.version.major
36 }
37
38 pub fn minor(&self) -> u64 {
40 self.version.minor
41 }
42
43 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}