hemtt_version/
lib.rs

1#![deny(clippy::all, clippy::nursery, missing_docs)]
2#![warn(clippy::pedantic)]
3
4//! Versioning for Arma mods
5
6mod error;
7use std::fmt::{Display, Formatter};
8
9pub use error::Error;
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11
12// which just had to be different from Semver for some reason
13/// Arma mod version format
14/// Examples of valid version:
15/// - 1.0.0.0-d1a631b1
16/// - 1.3.24.2452-1a2b3c4d
17/// - 1.2.42-1a2b3c4d
18/// - 1.2.42.2452
19/// - 1.2.42
20#[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    /// Create a new version
31    #[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    /// Read a version from a `script_version.hpp` files using macros
43    ///
44    /// ```hpp
45    /// #define MAJOR 3
46    /// #define MINOR 15
47    /// #define PATCHLVL 2
48    /// #define BUILD 69
49    /// ```
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the file does not contain the correct macros
54    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    /// Set the build number
66    pub fn set_build(&mut self, build: impl Into<String>) {
67        self.hash = Some(build.into());
68    }
69
70    /// Major version number
71    #[must_use]
72    pub const fn major(&self) -> u32 {
73        self.major
74    }
75
76    /// Minor version number
77    #[must_use]
78    pub const fn minor(&self) -> u32 {
79        self.minor
80    }
81
82    /// Patch version number
83    #[must_use]
84    pub const fn patch(&self) -> u32 {
85        self.patch
86    }
87
88    /// Build number
89    #[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        // remove comment
107        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}