1#![deny(clippy::all, clippy::nursery, missing_docs)]
2#![warn(clippy::pedantic)]
3
4mod error;
7use std::fmt::{Display, Formatter};
8
9pub use error::Error;
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11
12#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct Version {
22 major: u32,
23 minor: u32,
24 patch: u32,
25 build: Option<u32>,
26 hash: Option<String>,
27}
28
29impl Version {
30 #[must_use]
32 pub const fn new(major: u32, minor: u32, patch: u32, build: Option<u32>) -> Self {
33 Self {
34 major,
35 minor,
36 patch,
37 build,
38 hash: None,
39 }
40 }
41
42 pub fn try_from_script_version(version: &str) -> Result<Self, Error> {
55 let lines = version.lines().map(str::trim).collect::<Vec<_>>();
56 Ok(Self {
57 major: Self::extract_version(&lines, "MAJOR")?,
58 minor: Self::extract_version(&lines, "MINOR")?,
59 patch: Self::extract_version(&lines, "PATCH")?,
60 build: Self::extract_version(&lines, "BUILD").ok(),
61 hash: None,
62 })
63 }
64
65 pub fn set_build(&mut self, build: impl Into<String>) {
67 self.hash = Some(build.into());
68 }
69
70 #[must_use]
72 pub const fn major(&self) -> u32 {
73 self.major
74 }
75
76 #[must_use]
78 pub const fn minor(&self) -> u32 {
79 self.minor
80 }
81
82 #[must_use]
84 pub const fn patch(&self) -> u32 {
85 self.patch
86 }
87
88 #[must_use]
90 pub const fn build(&self) -> Option<u32> {
91 self.build
92 }
93
94 fn extract_version(lines: &[&str], component: &str) -> Result<u32, Error> {
95 let error = match component {
96 "MAJOR" => Error::ExpectedMajor,
97 "MINOR" => Error::ExpectedMinor,
98 "PATCH" => Error::ExpectedPatch,
99 "BUILD" => Error::ExpectedBuild,
100 _ => unreachable!(),
101 };
102 let line = lines
103 .iter()
104 .find(|line| line.starts_with(&format!("#define {component}")))
105 .ok_or_else(|| error.clone())?;
106 let component = line
108 .split_once("//")
109 .unwrap_or((line, ""))
110 .0
111 .trim()
112 .rsplit_once(' ')
113 .ok_or(error)?;
114 component
115 .1
116 .parse::<u32>()
117 .map_err(|_| Error::InvalidComponent(component.1.to_string()))
118 }
119}
120
121impl TryFrom<&str> for Version {
122 type Error = Error;
123
124 fn try_from(version: &str) -> Result<Self, Self::Error> {
125 let mut parts = version.split('-');
126 let mut version = parts.next().unwrap().split('.');
127 let Some(major) = version.next() else {
128 return Err(Error::ExpectedMajor);
129 };
130 let Ok(major) = major.parse() else {
131 return Err(Error::InvalidComponent(major.to_string()));
132 };
133 let Some(minor) = version.next() else {
134 return Err(Error::ExpectedMinor);
135 };
136 let Ok(minor) = minor.parse() else {
137 return Err(Error::InvalidComponent(minor.to_string()));
138 };
139 let Some(patch) = version.next() else {
140 return Err(Error::ExpectedPatch);
141 };
142 let Ok(patch) = patch.parse() else {
143 return Err(Error::InvalidComponent(patch.to_string()));
144 };
145 let build = version.next().map(|build| {
146 build
147 .parse::<u32>()
148 .map_err(|_| Error::InvalidComponent(build.to_string()))
149 });
150 let build = if let Some(build) = build {
151 Some(build?)
152 } else {
153 None
154 };
155 let hash = parts.next().map(std::string::ToString::to_string);
156 Ok(Self {
157 major,
158 minor,
159 patch,
160 build,
161 hash,
162 })
163 }
164}
165
166impl Serialize for Version {
167 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
168 where
169 S: Serializer,
170 {
171 let mut version = format!("{}.{}.{}", self.major, self.minor, self.patch);
172 if let Some(build) = self.build {
173 version.push_str(&format!(".{build}"));
174 }
175 if let Some(hash) = &self.hash {
176 version.push_str(&format!("-{hash}"));
177 }
178 serializer.serialize_str(&version)
179 }
180}
181
182impl<'de> Deserialize<'de> for Version {
183 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184 where
185 D: Deserializer<'de>,
186 {
187 let version = String::deserialize(deserializer)?;
188 Self::try_from(version.as_str()).map_err(serde::de::Error::custom)
189 }
190}
191
192impl Display for Version {
193 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
194 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
195 if let Some(build) = self.build {
196 write!(f, ".{build}")?;
197 }
198 if let Some(hash) = &self.hash {
199 write!(f, "-{hash}")?;
200 }
201 Ok(())
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_version() {
211 let version = Version::try_from("1.0.0.0-d1a631b1").unwrap();
212 assert_eq!(version.major, 1);
213 assert_eq!(version.minor, 0);
214 assert_eq!(version.patch, 0);
215 assert_eq!(version.build, Some(0));
216 assert_eq!(version.hash, Some("d1a631b1".to_string()));
217 }
218
219 #[test]
220 fn test_version_no_build() {
221 let version = Version::try_from("1.2.42-1a2b3c4d").unwrap();
222 assert_eq!(version.major, 1);
223 assert_eq!(version.minor, 2);
224 assert_eq!(version.patch, 42);
225 assert_eq!(version.build, None);
226 assert_eq!(version.hash, Some("1a2b3c4d".to_string()));
227 }
228
229 #[test]
230 fn test_version_no_hash() {
231 let version = Version::try_from("1.2.42.2452").unwrap();
232 assert_eq!(version.major, 1);
233 assert_eq!(version.minor, 2);
234 assert_eq!(version.patch, 42);
235 assert_eq!(version.build, Some(2452));
236 assert_eq!(version.hash, None);
237 }
238
239 #[test]
240 fn test_version_no_build_no_hash() {
241 let version = Version::try_from("1.2.42").unwrap();
242 assert_eq!(version.major, 1);
243 assert_eq!(version.minor, 2);
244 assert_eq!(version.patch, 42);
245 assert_eq!(version.build, None);
246 assert_eq!(version.hash, None);
247 }
248
249 #[test]
250 fn test_version_invalid_component() {
251 let version = Version::try_from("1.2.a");
252 assert!(version.is_err());
253 assert_eq!(
254 version.unwrap_err(),
255 Error::InvalidComponent("a".to_string())
256 );
257 }
258
259 #[test]
260 fn test_version_missing_minor() {
261 let version = Version::try_from("1");
262 assert!(version.is_err());
263 assert_eq!(version.unwrap_err(), Error::ExpectedMinor);
264 }
265
266 #[test]
267 fn test_version_missing_patch() {
268 let version = Version::try_from("1.2");
269 assert!(version.is_err());
270 assert_eq!(version.unwrap_err(), Error::ExpectedPatch);
271 }
272
273 #[test]
274 fn test_script_version() {
275 let content = r#"
276 #define MAJOR 1
277 #define MINOR 2
278 #define PATCH 3
279 #define BUILD 4
280 "#;
281 let version = Version::try_from_script_version(content).unwrap();
282 assert_eq!(version.major, 1);
283 assert_eq!(version.minor, 2);
284 assert_eq!(version.patch, 3);
285 assert_eq!(version.build, Some(4));
286
287 assert_eq!(version.hash, None);
288 }
289
290 #[test]
291 fn test_script_version_comment() {
292 let content = r#"
293 #define MAJOR 1
294 #define MINOR 2
295 #define PATCHLVL 3 // some comment
296 #define BUILD 4
297 "#;
298 let version = Version::try_from_script_version(content).unwrap();
299 assert_eq!(version.major, 1);
300 assert_eq!(version.minor, 2);
301 assert_eq!(version.patch, 3);
302 assert_eq!(version.build, Some(4));
303 assert_eq!(version.hash, None);
304 }
305
306 #[test]
307 fn test_script_version_no_build() {
308 let content = r#"
309 #define MAJOR 1
310 #define MINOR 2
311 #define PATCH 3
312 "#;
313 let version = Version::try_from_script_version(content).unwrap();
314 assert_eq!(version.major, 1);
315 assert_eq!(version.minor, 2);
316 assert_eq!(version.patch, 3);
317 assert_eq!(version.build, None);
318 assert_eq!(version.hash, None);
319 }
320
321 #[test]
322 fn test_script_version_invalid_component() {
323 let content = r#"
324 #define MAJOR 1
325 #define MINOR 2
326 #define PATCHLVL a
327 "#;
328 let version = Version::try_from_script_version(content);
329 assert!(version.is_err());
330 assert_eq!(
331 version.unwrap_err(),
332 Error::InvalidComponent("a".to_string())
333 );
334 }
335
336 #[test]
337 fn test_script_version_missing_minor() {
338 let content = r#"
339 #define MAJOR 1
340 "#;
341 let version = Version::try_from_script_version(content);
342 assert!(version.is_err());
343 assert_eq!(version.unwrap_err(), Error::ExpectedMinor);
344 }
345
346 #[test]
347 fn test_script_version_missing_patch() {
348 let content = r#"
349 #define MAJOR 1
350 #define MINOR 2
351 "#;
352 let version = Version::try_from_script_version(content);
353 assert!(version.is_err());
354 assert_eq!(version.unwrap_err(), Error::ExpectedPatch);
355 }
356
357 #[test]
358 fn test_script_version_missing_major() {
359 let content = r#"
360 #define MINOR 2
361 #define PATCH 3
362 "#;
363 let version = Version::try_from_script_version(content);
364 assert!(version.is_err());
365 assert_eq!(version.unwrap_err(), Error::ExpectedMajor);
366 }
367}