1use rootcause::{Report, report};
2use serde::de::{self, Visitor};
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::cmp::Ordering;
5use std::fmt::{Display, Formatter};
6use std::str::FromStr;
7
8#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
10#[error("{message}")]
11pub struct ParseVersionError {
12 message: String,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct Version {
18 pub major: u32,
20
21 pub minor: u32,
23
24 pub patch: u32,
26
27 pub build: u32,
29}
30
31impl Display for Version {
32 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
33 f.write_fmt(format_args!(
34 "{}.{}.{}.{}",
35 self.major, self.minor, self.patch, self.build
36 ))
37 }
38}
39
40impl Ord for Version {
41 fn cmp(&self, other: &Self) -> Ordering {
42 self.major
43 .cmp(&other.major)
44 .then(self.minor.cmp(&other.minor))
45 .then(self.patch.cmp(&other.patch))
46 .then(self.build.cmp(&other.build))
47 }
48}
49
50impl PartialOrd for Version {
51 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52 Some(self.cmp(other))
53 }
54}
55
56fn parse_version(value: &str) -> Result<Version, String> {
57 fn parse_part<'i>(
58 parts: &mut impl Iterator<Item = &'i str>,
59 named: &'static str,
60 ) -> Result<u32, String> {
61 parts
62 .next()
63 .ok_or_else(|| format!("Did not find part '{named}'."))?
64 .parse::<u32>()
65 .map_err(|err| format!("Failed to parse '{named}' part as an u32: {err}"))
66 }
67
68 let mut parts = value.split('.');
69 let major = parse_part(&mut parts, "major")?;
70 let minor = parse_part(&mut parts, "minor")?;
71 let patch = parse_part(&mut parts, "patch")?;
72 let build = parse_part(&mut parts, "build")?;
73
74 if let Some(next) = parts.next() {
75 return Err(format!(
76 "Invalid version string format. Did not expect any additional parts. Got at least the additional part: {next}"
77 ));
78 }
79
80 Ok(Version {
81 major,
82 minor,
83 patch,
84 build,
85 })
86}
87
88impl FromStr for Version {
89 type Err = Report<ParseVersionError>;
90
91 fn from_str(s: &str) -> Result<Self, Self::Err> {
92 parse_version(s).map_err(|message| report!(ParseVersionError { message }))
93 }
94}
95
96impl Serialize for Version {
97 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
98 where
99 S: Serializer,
100 {
101 serializer.serialize_str(&self.to_string())
102 }
103}
104
105impl<'de> Deserialize<'de> for Version {
106 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
107 where
108 D: Deserializer<'de>,
109 {
110 struct VersionVisitor;
111
112 impl Visitor<'_> for VersionVisitor {
113 type Value = Version;
114
115 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
116 formatter.write_str("a version string in dot format \"{major}.{minor}.{patch}.{build}\", like `1.0.0.0`, with each part being a `u32`, and all parts being required")
117 }
118
119 fn visit_str<E>(self, value: &str) -> Result<Version, E>
120 where
121 E: de::Error,
122 {
123 parse_version(value).map_err(de::Error::custom)
124 }
125 }
126
127 deserializer.deserialize_str(VersionVisitor)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use assertr::prelude::*;
135 use std::collections::HashSet;
136
137 #[test]
138 fn deserialize_valid_version() {
139 let test_cases = vec![
140 (
141 "0.0.0.0",
142 Version {
143 major: 0,
144 minor: 0,
145 patch: 0,
146 build: 0,
147 },
148 ),
149 (
150 "1.2.3.4",
151 Version {
152 major: 1,
153 minor: 2,
154 patch: 3,
155 build: 4,
156 },
157 ),
158 (
159 "115.785.5763.42",
160 Version {
161 major: 115,
162 minor: 785,
163 patch: 5763,
164 build: 42,
165 },
166 ),
167 (
168 "4294967295.4294967295.4294967295.4294967295",
169 Version {
170 major: u32::MAX,
171 minor: u32::MAX,
172 patch: u32::MAX,
173 build: u32::MAX,
174 },
175 ),
176 ];
177
178 for (input, expected) in test_cases {
179 let json = format!(
180 r#"
181 "{input}"
182 "#
183 );
184 let result = serde_json::from_str::<Version>(&json);
185 assert_that!(result).is_ok().is_equal_to(expected);
186 }
187 }
188
189 #[test]
190 fn deserialize_invalid_version() {
191 let invalid_cases = vec![
192 ("", "Failed to parse 'major' part as an u32"),
193 ("1", "Did not find part 'minor'"),
194 ("1.2", "Did not find part 'patch'"),
195 ("1.2.3", "Did not find part 'build'"),
196 ("1.2.3.4.5", "Did not expect any additional parts"),
197 ("a.b.c.d", "Failed to parse 'major' part as an u32"),
198 ("1.b.c.d", "Failed to parse 'minor' part as an u32"),
199 ("1.2.c.d", "Failed to parse 'patch' part as an u32"),
200 ("1.2.3.d", "Failed to parse 'build' part as an u32"),
201 ("-1.2.3.4", "Failed to parse 'major' part as an u32"),
202 ("1.-2.3.4", "Failed to parse 'minor' part as an u32"),
203 ("1.2.-3.4", "Failed to parse 'patch' part as an u32"),
204 ("1.2.3.-4", "Failed to parse 'build' part as an u32"),
205 ("4294967296.0.0.0", "Failed to parse 'major' part as an u32"),
206 ("0.4294967296.0.0", "Failed to parse 'minor' part as an u32"),
207 ("0.0.4294967296.0", "Failed to parse 'patch' part as an u32"),
208 ("0.0.0.4294967296", "Failed to parse 'build' part as an u32"),
209 (".2.3.4", "Failed to parse 'major' part as an u32"),
210 ("1..3.4", "Failed to parse 'minor' part as an u32"),
211 ("1.2..4", "Failed to parse 'patch' part as an u32"),
212 ("1.2.3.", "Failed to parse 'build' part as an u32"),
213 ];
214
215 for (input, expected_error_substring) in invalid_cases {
216 let json = format!(
217 r#"
218 "{input}"
219 "#
220 );
221 let result = serde_json::from_str::<Version>(&json);
222 assert_that!(result)
223 .is_err()
224 .derive(std::string::ToString::to_string)
225 .contains(expected_error_substring);
226 }
227 }
228
229 #[test]
230 fn display_value() {
231 let version = Version {
232 major: 115,
233 minor: 785,
234 patch: 5763,
235 build: 42,
236 };
237 assert_that!(version).has_display_value("115.785.5763.42");
238 }
239
240 #[test]
241 fn debug_value() {
242 let version = Version {
243 major: 115,
244 minor: 785,
245 patch: 5763,
246 build: 42,
247 };
248 assert_that!(version)
249 .has_debug_value("Version { major: 115, minor: 785, patch: 5763, build: 42 }");
250 }
251
252 #[test]
253 fn comparisons() {
254 let v1 = Version {
255 major: 1,
256 minor: 0,
257 patch: 0,
258 build: 0,
259 };
260 let v2 = Version {
261 major: 1,
262 minor: 0,
263 patch: 0,
264 build: 1,
265 };
266 let v3 = Version {
267 major: 1,
268 minor: 0,
269 patch: 1,
270 build: 0,
271 };
272 let v4 = Version {
273 major: 1,
274 minor: 1,
275 patch: 0,
276 build: 0,
277 };
278 let v5 = Version {
279 major: 2,
280 minor: 0,
281 patch: 0,
282 build: 0,
283 };
284
285 assert_that!(v1).is_less_than(v2);
286 assert_that!(v1).is_less_than(v3);
287 assert_that!(v1).is_less_than(v4);
288 assert_that!(v1).is_less_than(v5);
289 assert_that!(v2).is_less_than(v3);
290 assert_that!(v2).is_less_than(v4);
291 assert_that!(v2).is_less_than(v5);
292 assert_that!(v3).is_less_than(v4);
293 assert_that!(v3).is_less_than(v5);
294 assert_that!(v4).is_less_than(v5);
295
296 assert_that!(v2).is_greater_than(v1);
297 assert_that!(v3).is_greater_than(v1);
298 assert_that!(v4).is_greater_than(v1);
299 assert_that!(v5).is_greater_than(v1);
300 assert_that!(v3).is_greater_than(v2);
301 assert_that!(v4).is_greater_than(v2);
302 assert_that!(v5).is_greater_than(v2);
303 assert_that!(v4).is_greater_than(v3);
304 assert_that!(v5).is_greater_than(v3);
305 assert_that!(v5).is_greater_than(v4);
306
307 assert_that!(v1).is_equal_to(v1);
308 assert_that!(v1).is_less_or_equal_to(v1);
309 assert_that!(v1).is_greater_or_equal_to(v1);
310
311 assert_that!(v2).is_equal_to(v2);
312 assert_that!(v2).is_less_or_equal_to(v2);
313 assert_that!(v2).is_greater_or_equal_to(v2);
314
315 assert_that!(v3).is_equal_to(v3);
316 assert_that!(v3).is_less_or_equal_to(v3);
317 assert_that!(v3).is_greater_or_equal_to(v3);
318
319 assert_that!(v4).is_equal_to(v4);
320 assert_that!(v4).is_less_or_equal_to(v4);
321 assert_that!(v4).is_greater_or_equal_to(v4);
322
323 assert_that!(v5).is_equal_to(v5);
324 assert_that!(v5).is_less_or_equal_to(v5);
325 assert_that!(v5).is_greater_or_equal_to(v5);
326 }
327
328 #[test]
329 fn parse_valid() {
330 assert_that!("1.2.3.4".parse::<Version>())
331 .is_ok()
332 .is_equal_to(Version {
333 major: 1,
334 minor: 2,
335 patch: 3,
336 build: 4,
337 });
338 assert_that!("0.0.0.0".parse::<Version>())
339 .is_ok()
340 .is_equal_to(Version {
341 major: 0,
342 minor: 0,
343 patch: 0,
344 build: 0,
345 });
346 }
347
348 #[test]
349 fn parse_invalid_fails() {
350 assert_that!("".parse::<Version>()).is_err();
351 assert_that!("1".parse::<Version>()).is_err();
352 assert_that!("1.2".parse::<Version>()).is_err();
353 assert_that!("1.2.3".parse::<Version>()).is_err();
354 assert_that!("1.2.3.4.5".parse::<Version>()).is_err();
355 assert_that!("a.b.c.d".parse::<Version>()).is_err();
356 }
357
358 #[test]
359 fn serialize_round_trip() {
360 let version = Version {
361 major: 132,
362 minor: 0,
363 patch: 6834,
364 build: 83,
365 };
366 let json = serde_json::to_string(&version).unwrap();
367 assert_that!(json.clone()).is_equal_to(String::from("\"132.0.6834.83\""));
368 let deserialized: Version = serde_json::from_str(&json).unwrap();
369 assert_that!(deserialized).is_equal_to(version);
370 }
371
372 #[test]
373 fn ord_sorting() {
374 let v3 = Version {
375 major: 2,
376 minor: 0,
377 patch: 0,
378 build: 0,
379 };
380 let v1 = Version {
381 major: 1,
382 minor: 0,
383 patch: 0,
384 build: 0,
385 };
386 let v2 = Version {
387 major: 1,
388 minor: 1,
389 patch: 0,
390 build: 0,
391 };
392 let mut versions = vec![v3, v1, v2];
393 versions.sort();
394 assert_that!(versions).is_equal_to(vec![v1, v2, v3]);
395 }
396
397 #[test]
398 fn hash_in_set() {
399 let v1 = Version {
400 major: 1,
401 minor: 0,
402 patch: 0,
403 build: 0,
404 };
405 let v2 = Version {
406 major: 1,
407 minor: 0,
408 patch: 0,
409 build: 0,
410 };
411 let mut set = HashSet::new();
412 set.insert(v1);
413 set.insert(v2);
414 assert_that!(set.len()).is_equal_to(1);
415 }
416}