facti_lib/
version.rs

1use std::{
2    fmt::{self, Display, Formatter},
3    str::FromStr,
4};
5
6use crate::error::{ParseVersionReqError, ParseVersionSpecError};
7
8use super::error::ParseVersionError;
9
10mod factorio_version;
11
12pub use factorio_version::FactorioVersion;
13
14/// Represents a mod's version, in (limited) semver format.
15///
16/// # Examples
17///
18/// ```
19/// use facti_lib::version::Version;
20///
21/// let my_version = Version { major: 1, minor: 2, patch: 3 };
22///
23/// println!("My version is: {}", my_version);
24/// ```
25#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
26pub struct Version {
27    /// The major part of the version number.
28    pub major: u64,
29
30    /// The minor part of the version number.
31    pub minor: u64,
32
33    /// The patch part of the version number.
34    ///
35    /// Factorio documentation and resources sometimes refer to this field
36    /// as the "sub" part.
37    pub patch: u64,
38}
39
40impl Version {
41    pub fn new(major: u64, minor: u64, patch: u64) -> Self {
42        Self {
43            major,
44            minor,
45            patch,
46        }
47    }
48
49    /// Parses a version string into a [`Version`].
50    pub fn parse(s: &str) -> Result<Self, ParseVersionError> {
51        s.parse()
52    }
53
54    /// Checks if this [`Version`] is compatible with the given [`VersionSpec`].
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// # use facti_lib::version::{Version, VersionSpec};
60    /// assert!(Version::new(1, 2, 3).matches(VersionSpec::parse("= 1.2.3")?));
61    /// assert!(Version::new(4, 1, 3).matches(VersionSpec::parse("> 4.0.0")?));
62    /// assert!(Version::new(0, 5, 1).matches(VersionSpec::parse("<= 1.0.0")?));
63    /// # Ok::<(), facti_lib::error::ParseVersionSpecError>(())
64    /// ```
65    pub fn matches(&self, spec: VersionSpec) -> bool {
66        spec.matches(*self)
67    }
68}
69
70impl FromStr for Version {
71    type Err = ParseVersionError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        let parts = s.trim().split('.').map(|p| p.trim()).collect::<Vec<_>>();
75
76        if parts.len() != 3 {
77            return Err(ParseVersionError::Size(3, parts.len()));
78        }
79
80        let major = parts[0].parse().map_err(ParseVersionError::Major)?;
81        let minor = parts[1].parse().map_err(ParseVersionError::Minor)?;
82        let patch = parts[2].parse().map_err(ParseVersionError::Patch)?;
83
84        Ok(Version::new(major, minor, patch))
85    }
86}
87
88impl Display for Version {
89    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
90        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
91    }
92}
93
94impl From<FactorioVersion> for Version {
95    fn from(value: FactorioVersion) -> Self {
96        Self::new(value.major, value.minor, value.patch.unwrap_or(0))
97    }
98}
99
100#[derive(Copy, Clone, Debug, Eq, PartialEq)]
101pub enum Op {
102    Exact,
103    Greater,
104    GreaterEq,
105    Less,
106    LessEq,
107}
108
109impl Display for Op {
110    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
111        f.write_str(match self {
112            Op::Exact => "=",
113            Op::Greater => ">",
114            Op::GreaterEq => ">=",
115            Op::Less => "<",
116            Op::LessEq => "<=",
117        })
118    }
119}
120
121/// Represents a version requirement.
122#[derive(Copy, Clone, Debug, Eq, PartialEq)]
123pub enum VersionReq {
124    /// Used when the latest version is desired.
125    Latest,
126
127    /// Used when a specific version is required,
128    /// or one that matches a predicate.
129    Spec(VersionSpec),
130}
131
132impl VersionReq {
133    /// Parses a string into a [`VersionReq`].
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// # use facti_lib::version::VersionReq;
139    /// let exact_req = VersionReq::parse("= 1.2.3")?;
140    /// let latest = VersionReq::parse("")?;
141    /// # Ok::<(), facti_lib::error::ParseVersionReqError>(())
142    pub fn parse(s: &str) -> Result<Self, ParseVersionReqError> {
143        s.parse()
144    }
145}
146
147impl FromStr for VersionReq {
148    type Err = ParseVersionReqError;
149
150    fn from_str(s: &str) -> Result<Self, Self::Err> {
151        let trimmed = s.trim();
152        if trimmed.is_empty() {
153            return Ok(VersionReq::Latest);
154        }
155
156        let spec: VersionSpec = trimmed.parse()?;
157
158        Ok(VersionReq::Spec(spec))
159    }
160}
161
162impl Display for VersionReq {
163    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
164        match self {
165            VersionReq::Latest => f.write_str(""),
166            VersionReq::Spec(spec) => spec.fmt(f),
167        }
168    }
169}
170
171/// Specifies a specific require version, or a version matching
172/// a given predicate.
173#[derive(Copy, Clone, Debug, Eq, PartialEq)]
174pub struct VersionSpec {
175    /// The predicate/operator to use when matching versions.
176    pub op: Op,
177
178    /// The version to use as baseline.
179    pub version: Version,
180}
181
182impl VersionSpec {
183    pub fn new(op: Op, version: Version) -> Self {
184        Self { op, version }
185    }
186
187    /// Parses a string into a [`VersionSpec`].
188    pub fn parse(s: &str) -> Result<Self, ParseVersionSpecError> {
189        s.parse()
190    }
191
192    /// Checks if the given [`Version`] matches this [`VersionSpec`].
193    pub fn matches(&self, version: Version) -> bool {
194        match self.op {
195            Op::Exact => self.version == version,
196            Op::Greater => self.version < version,
197            Op::GreaterEq => self.version <= version,
198            Op::Less => self.version > version,
199            Op::LessEq => self.version >= version,
200        }
201    }
202}
203
204impl FromStr for VersionSpec {
205    type Err = ParseVersionSpecError;
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        let semver_req = semver::VersionReq::parse(s)?;
209        semver_req.try_into()
210    }
211}
212
213impl Display for VersionSpec {
214    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
215        f.write_str(&format!("{} {}", self.op, self.version))
216    }
217}
218
219impl From<FactorioVersion> for VersionSpec {
220    fn from(value: FactorioVersion) -> Self {
221        Self::new(Op::GreaterEq, value.into())
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    macro_rules! test_specs {
230        ($($name:ident($spec:literal, $version:literal, $expected:expr);)*) => {
231            $(
232                #[test]
233                fn $name() {
234                    let spec = VersionSpec::parse($spec).unwrap();
235                    let version = Version::parse($version).unwrap();
236                    assert_eq!(spec.matches(version), $expected, "expected {} when matching {version} against {spec}", $expected);
237                }
238            )*
239        };
240    }
241
242    test_specs! {
243        same_version_matches_exact("= 1.2.3", "1.2.3", true);
244        diff_major_does_not_match_exact("= 1.2.3", "2.2.3", false);
245        diff_minor_does_not_match_exact("= 1.2.3", "1.3.3", false);
246        diff_patch_does_not_match_exact("= 1.2.3", "1.2.4", false);
247        larger_major_matches_greater("> 1.2.3", "5.2.3", true);
248        larger_minor_matches_greater("> 1.2.3", "1.5.3", true);
249        larger_patch_matches_greater("> 1.2.3", "1.2.5", true);
250        smaller_major_does_not_match_greater("> 1.2.3", "0.2.3", false);
251        smaller_minor_greater_major_matches_greater("> 1.2.3", "3.1.3", true);
252        smaller_patch_greater_major_matches_greater("> 1.2.3", "4.1.0", true);
253    }
254}