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#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
26pub struct Version {
27 pub major: u64,
29
30 pub minor: u64,
32
33 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 pub fn parse(s: &str) -> Result<Self, ParseVersionError> {
51 s.parse()
52 }
53
54 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#[derive(Copy, Clone, Debug, Eq, PartialEq)]
123pub enum VersionReq {
124 Latest,
126
127 Spec(VersionSpec),
130}
131
132impl VersionReq {
133 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#[derive(Copy, Clone, Debug, Eq, PartialEq)]
174pub struct VersionSpec {
175 pub op: Op,
177
178 pub version: Version,
180}
181
182impl VersionSpec {
183 pub fn new(op: Op, version: Version) -> Self {
184 Self { op, version }
185 }
186
187 pub fn parse(s: &str) -> Result<Self, ParseVersionSpecError> {
189 s.parse()
190 }
191
192 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}