1use serde::de::{self, Visitor};
2use serde::{Deserialize, Deserializer};
3use std::cmp::PartialOrd;
4use std::fmt::{Display, Formatter};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct Version {
9 pub major: u32,
11
12 pub minor: u32,
14
15 pub patch: u32,
17
18 pub build: u32,
20}
21
22impl Display for Version {
23 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24 f.write_fmt(format_args!(
25 "{}.{}.{}.{}",
26 self.major, self.minor, self.patch, self.build
27 ))
28 }
29}
30
31impl PartialOrd for Version {
32 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
33 if self.major != other.major {
34 return self.major.partial_cmp(&other.major);
35 }
36 if self.minor != other.minor {
37 return self.minor.partial_cmp(&other.minor);
38 }
39 if self.patch != other.patch {
40 return self.patch.partial_cmp(&other.patch);
41 }
42 if self.build != other.build {
43 return self.build.partial_cmp(&other.build);
44 }
45 Some(std::cmp::Ordering::Equal)
46 }
47
48 fn lt(&self, other: &Self) -> bool {
49 self.partial_cmp(other) == Some(std::cmp::Ordering::Less)
50 }
51
52 fn le(&self, other: &Self) -> bool {
53 self.partial_cmp(other) != Some(std::cmp::Ordering::Greater)
54 }
55
56 fn gt(&self, other: &Self) -> bool {
57 self.partial_cmp(other) == Some(std::cmp::Ordering::Greater)
58 }
59
60 fn ge(&self, other: &Self) -> bool {
61 self.partial_cmp(other) != Some(std::cmp::Ordering::Less)
62 }
63}
64
65impl<'de> Deserialize<'de> for Version {
66 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67 where
68 D: Deserializer<'de>,
69 {
70 struct VersionVisitor;
71
72 impl Visitor<'_> for VersionVisitor {
73 type Value = Version;
74
75 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
76 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")
77 }
78
79 fn visit_str<E>(self, value: &str) -> Result<Version, E>
80 where
81 E: de::Error,
82 {
83 let mut parts = value.split('.');
84
85 fn parse_part<'i, E: de::Error>(
86 parts: &mut impl Iterator<Item = &'i str>,
87 named: &'static str,
88 ) -> Result<u32, E> {
89 parts
90 .next()
91 .ok_or_else(|| de::Error::custom(format!("Did not find part '{named}'.")))?
92 .parse::<u32>()
93 .map_err(|err| {
94 de::Error::custom(format!(
95 "Failed to parse '{named}' part as an u32: {err}"
96 ))
97 })
98 }
99
100 let major = parse_part(&mut parts, "major")?;
101 let minor = parse_part(&mut parts, "minor")?;
102 let patch = parse_part(&mut parts, "patch")?;
103 let build = parse_part(&mut parts, "build")?;
104
105 if let Some(next) = parts.next() {
106 return Err(de::Error::custom(format!(
107 "Invalid version string format. Did not expect any additional parts. Got at least the additional part: {next}"
108 )));
109 }
110
111 Ok(Version {
112 major,
113 minor,
114 patch,
115 build,
116 })
117 }
118 }
119
120 deserializer.deserialize_str(VersionVisitor)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use assertr::prelude::*;
128
129 #[test]
130 fn deserialize_valid_version() {
131 let test_cases = vec![
132 (
133 "0.0.0.0",
134 Version {
135 major: 0,
136 minor: 0,
137 patch: 0,
138 build: 0,
139 },
140 ),
141 (
142 "1.2.3.4",
143 Version {
144 major: 1,
145 minor: 2,
146 patch: 3,
147 build: 4,
148 },
149 ),
150 (
151 "115.785.5763.42",
152 Version {
153 major: 115,
154 minor: 785,
155 patch: 5763,
156 build: 42,
157 },
158 ),
159 (
160 "4294967295.4294967295.4294967295.4294967295",
161 Version {
162 major: u32::MAX,
163 minor: u32::MAX,
164 patch: u32::MAX,
165 build: u32::MAX,
166 },
167 ),
168 ];
169
170 for (input, expected) in test_cases {
171 let json = format!(
172 r#"
173 "{input}"
174 "#
175 );
176 let result = serde_json::from_str::<Version>(&json);
177 assert_that(result).is_ok().is_equal_to(expected);
178 }
179 }
180
181 #[test]
182 fn deserialize_invalid_version() {
183 let invalid_cases = vec![
184 ("", "Failed to parse 'major' part as an u32"),
185 ("1", "Did not find part 'minor'"),
186 ("1.2", "Did not find part 'patch'"),
187 ("1.2.3", "Did not find part 'build'"),
188 ("1.2.3.4.5", "Did not expect any additional parts"),
189 ("a.b.c.d", "Failed to parse 'major' part as an u32"),
190 ("1.b.c.d", "Failed to parse 'minor' part as an u32"),
191 ("1.2.c.d", "Failed to parse 'patch' part as an u32"),
192 ("1.2.3.d", "Failed to parse 'build' part as an u32"),
193 ("-1.2.3.4", "Failed to parse 'major' part as an u32"),
194 ("1.-2.3.4", "Failed to parse 'minor' part as an u32"),
195 ("1.2.-3.4", "Failed to parse 'patch' part as an u32"),
196 ("1.2.3.-4", "Failed to parse 'build' part as an u32"),
197 ("4294967296.0.0.0", "Failed to parse 'major' part as an u32"),
198 ("0.4294967296.0.0", "Failed to parse 'minor' part as an u32"),
199 ("0.0.4294967296.0", "Failed to parse 'patch' part as an u32"),
200 ("0.0.0.4294967296", "Failed to parse 'build' part as an u32"),
201 (".2.3.4", "Failed to parse 'major' part as an u32"),
202 ("1..3.4", "Failed to parse 'minor' part as an u32"),
203 ("1.2..4", "Failed to parse 'patch' part as an u32"),
204 ("1.2.3.", "Failed to parse 'build' part as an u32"),
205 ];
206
207 for (input, expected_error_substring) in invalid_cases {
208 let json = format!(
209 r#"
210 "{input}"
211 "#
212 );
213 let result = serde_json::from_str::<Version>(&json);
214 assert_that(result)
215 .is_err()
216 .derive(|it| it.to_string())
217 .contains(expected_error_substring);
218 }
219 }
220
221 #[test]
222 fn display_value() {
223 let version = Version {
224 major: 115,
225 minor: 785,
226 patch: 5763,
227 build: 42,
228 };
229 assert_that(version).has_display_value("115.785.5763.42");
230 }
231
232 #[test]
233 fn debug_value() {
234 let version = Version {
235 major: 115,
236 minor: 785,
237 patch: 5763,
238 build: 42,
239 };
240 assert_that(version)
241 .has_debug_value("Version { major: 115, minor: 785, patch: 5763, build: 42 }");
242 }
243
244 #[test]
245 fn comparisons() {
246 let v1 = Version {
247 major: 1,
248 minor: 0,
249 patch: 0,
250 build: 0,
251 };
252 let v2 = Version {
253 major: 1,
254 minor: 0,
255 patch: 0,
256 build: 1,
257 };
258 let v3 = Version {
259 major: 1,
260 minor: 0,
261 patch: 1,
262 build: 0,
263 };
264 let v4 = Version {
265 major: 1,
266 minor: 1,
267 patch: 0,
268 build: 0,
269 };
270 let v5 = Version {
271 major: 2,
272 minor: 0,
273 patch: 0,
274 build: 0,
275 };
276
277 assert_that(v1).is_less_than(v2);
278 assert_that(v1).is_less_than(v3);
279 assert_that(v1).is_less_than(v4);
280 assert_that(v1).is_less_than(v5);
281 assert_that(v2).is_less_than(v3);
282 assert_that(v2).is_less_than(v4);
283 assert_that(v2).is_less_than(v5);
284 assert_that(v3).is_less_than(v4);
285 assert_that(v3).is_less_than(v5);
286 assert_that(v4).is_less_than(v5);
287
288 assert_that(v2).is_greater_than(v1);
289 assert_that(v3).is_greater_than(v1);
290 assert_that(v4).is_greater_than(v1);
291 assert_that(v5).is_greater_than(v1);
292 assert_that(v3).is_greater_than(v2);
293 assert_that(v4).is_greater_than(v2);
294 assert_that(v5).is_greater_than(v2);
295 assert_that(v4).is_greater_than(v3);
296 assert_that(v5).is_greater_than(v3);
297 assert_that(v5).is_greater_than(v4);
298
299 assert_that(v1).is_equal_to(v1);
300 assert_that(v1).is_less_or_equal_to(v1);
301 assert_that(v1).is_greater_or_equal_to(v1);
302
303 assert_that(v2).is_equal_to(v2);
304 assert_that(v2).is_less_or_equal_to(v2);
305 assert_that(v2).is_greater_or_equal_to(v2);
306
307 assert_that(v3).is_equal_to(v3);
308 assert_that(v3).is_less_or_equal_to(v3);
309 assert_that(v3).is_greater_or_equal_to(v3);
310
311 assert_that(v4).is_equal_to(v4);
312 assert_that(v4).is_less_or_equal_to(v4);
313 assert_that(v4).is_greater_or_equal_to(v4);
314
315 assert_that(v5).is_equal_to(v5);
316 assert_that(v5).is_less_or_equal_to(v5);
317 assert_that(v5).is_greater_or_equal_to(v5);
318 }
319}