Skip to main content

chrome_for_testing/api/
version.rs

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