facti_lib/version/
factorio_version.rs

1use std::{
2    cmp,
3    fmt::{self, Display, Formatter},
4    str::FromStr,
5};
6
7use crate::error::ParseVersionError;
8
9/// Represents a version of Factorio (the game).
10///
11/// In most cases, the [`patch`][`FactorioVersion::patch`] field
12/// should be left as [`None`].
13///
14/// The game and its APIs may sometimes return a patch component,
15/// and some wrongly configured mods on the mod portal may also have it
16/// set (in error).
17///
18/// If you're constructing a [`ModInfo`][`facti_lib::ModInfo`] struct,
19/// you **MUST NOT** set the patch component, as that is considered invalid
20/// and the mod portal will reject your mod. It may also make the game behave
21/// in unexpected ways.
22#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
23pub struct FactorioVersion {
24    /// The major component of the version.
25    pub major: u64,
26
27    /// The minor component of the version.
28    pub minor: u64,
29
30    /// The patch component of the version, if any.
31    pub patch: Option<u64>,
32}
33
34impl FactorioVersion {
35    /// Constructs a [`FactorioVersion`] with just the [`major`][`FactorioVersion::major`]
36    /// and [`minor`][`FactorioVersion::minor`] fields set.
37    ///
38    /// This is most often the correct method to use.
39    pub fn new(major: u64, minor: u64) -> Self {
40        Self {
41            major,
42            minor,
43            patch: None,
44        }
45    }
46
47    pub fn with_patch(major: u64, minor: u64, patch: u64) -> Self {
48        Self {
49            major,
50            minor,
51            patch: Some(patch),
52        }
53    }
54
55    /// Parses a [`FactorioVersion`] from a string.
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// # use facti_lib::version::FactorioVersion;
61    /// let version = FactorioVersion::parse("1.2")?;
62    /// assert_eq!(version.major, 1);
63    /// assert_eq!(version.minor, 2);
64    /// assert!(version.patch.is_none());
65    ///
66    /// let with_patch = FactorioVersion::parse("1.2.3")?;
67    /// assert_eq!(with_patch.major, 1);
68    /// assert_eq!(with_patch.minor, 2);
69    /// assert_eq!(with_patch.patch, Some(3));
70    /// # Ok::<(), facti_lib::error::ParseVersionError>(())
71    /// ```
72    pub fn parse(s: &str) -> Result<Self, ParseVersionError> {
73        s.parse()
74    }
75
76    /// Constructs a potentially invalid Factorio version, which may include
77    /// a patch version.
78    ///
79    /// Normally this should not be possible, but some mods on the portal have
80    /// a patch version specified and will fail to parse if we don't allow it.
81    pub(crate) fn create(major: u64, minor: u64, patch: Option<u64>) -> Self {
82        Self {
83            major,
84            minor,
85            patch,
86        }
87    }
88}
89
90impl Default for FactorioVersion {
91    fn default() -> Self {
92        Self {
93            major: 0,
94            minor: 12,
95            patch: None,
96        }
97    }
98}
99
100impl Ord for FactorioVersion {
101    fn cmp(&self, other: &Self) -> cmp::Ordering {
102        use cmp::Ordering::*;
103        match self.major.cmp(&other.major) {
104            Equal => match self.minor.cmp(&other.minor) {
105                Equal => match (self.patch, other.patch) {
106                    (Some(self_patch), Some(other_patch)) => self_patch.cmp(&other_patch),
107                    (Some(_), None) => Less,
108                    (None, Some(_)) => Greater,
109                    (None, None) => Equal,
110                },
111                ordering => ordering,
112            },
113            ordering => ordering,
114        }
115    }
116}
117
118impl PartialOrd for FactorioVersion {
119    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
120        Some(self.cmp(other))
121    }
122}
123
124impl FromStr for FactorioVersion {
125    type Err = ParseVersionError;
126
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        let parts = s.trim().split('.').map(|p| p.trim()).collect::<Vec<_>>();
129
130        if parts.len() > 3 {
131            return Err(ParseVersionError::Size(2, parts.len()));
132        }
133
134        let major = parts[0].parse().map_err(ParseVersionError::Major)?;
135        let minor = parts[1].parse().map_err(ParseVersionError::Minor)?;
136
137        let patch: Option<u64> = if parts.len() == 3 {
138            Some(parts[2].parse().map_err(ParseVersionError::Patch)?)
139        } else {
140            None
141        };
142
143        Ok(FactorioVersion::create(major, minor, patch))
144    }
145}
146
147impl Display for FactorioVersion {
148    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
149        write!(f, "{}.{}", self.major, self.minor)?;
150
151        if let Some(patch) = self.patch {
152            write!(f, ".{}", patch)?;
153        }
154
155        Ok(())
156    }
157}
158
159#[cfg(test)]
160mod test_super {
161    use std::cmp;
162
163    use super::*;
164
165    #[test]
166    fn test_parse() {
167        assert_eq!(
168            FactorioVersion::parse("1.80").unwrap(),
169            FactorioVersion::new(1, 80)
170        );
171    }
172
173    #[test]
174    fn test_parse_patch() {
175        assert_eq!(
176            FactorioVersion::parse("1.80.66").unwrap(),
177            FactorioVersion::with_patch(1, 80, 66)
178        );
179    }
180
181    #[test]
182    fn test_display() {
183        assert_eq!(format!("{}", FactorioVersion::new(1, 2)), "1.2");
184    }
185
186    #[test]
187    fn test_display_patch() {
188        assert_eq!(format!("{}", FactorioVersion::with_patch(1, 2, 3)), "1.2.3");
189    }
190
191    #[test]
192    fn test_ordering() {
193        let mut major_differs = vec![FactorioVersion::new(5, 1), FactorioVersion::new(1, 2)];
194        major_differs.sort();
195        assert_eq!(
196            major_differs,
197            vec![FactorioVersion::new(1, 2), FactorioVersion::new(5, 1)]
198        );
199        let mut minor_differs = vec![FactorioVersion::new(1, 5), FactorioVersion::new(1, 2)];
200        minor_differs.sort();
201        assert_eq!(
202            minor_differs,
203            vec![FactorioVersion::new(1, 2), FactorioVersion::new(1, 5)]
204        );
205    }
206
207    /// Test that a version with a patch is greater than one without.
208    ///
209    /// Rationale: A [`FactorioVersion`] that has specified [`None`] for its
210    /// patch version means it wants the latest version of Factorio that'
211    /// matches the specified `major` and `minor` components. Thus the patch
212    /// ([`None`]) will always be the latest available, and no explicitly
213    /// stated `patch` value would be greater than that.
214    #[test]
215    fn test_nopatch_gt_haspatch() {
216        let no_patch = FactorioVersion::new(1, 2);
217        let has_patch = FactorioVersion::with_patch(1, 2, 0);
218
219        assert_eq!(no_patch.cmp(&has_patch), cmp::Ordering::Greater)
220    }
221}