1use std::cmp::Ordering;
6use std::fmt::{Display, Formatter};
7use std::hash::{Hash, Hasher};
8use std::path::Path;
9
10use lazy_static::lazy_static;
11use regex::{Captures, Match, Regex};
12use serde::{Deserialize, Serialize};
13
14use crate::error::TagKindError;
15
16const PROTON: &str = "PROTON";
17const WINE: &str = "WINE";
18const LOL_WINE: &str = "LOL_WINE";
19
20const RELEASE_CANDIDATE_MARKER: &str = "rc";
21const FIRST_GROUP: usize = 1;
22
23lazy_static! {
24 static ref NUMBERS: Regex = Regex::new(r"(\d+)").unwrap();
25 static ref TAG_MARKERS: Vec<String> = vec![String::from("rc"), String::from("LoL"), String::from("MF")];
26}
27
28#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
33pub struct SemVer {
34 major: u8,
35 minor: u8,
36 patch: u8,
37 identifier: Option<String>,
38}
39
40impl SemVer {
41 fn new(major: u8, minor: u8, patch: u8, identifier: Option<String>) -> Self {
42 SemVer {
43 major,
44 minor,
45 patch,
46 identifier,
47 }
48 }
49
50 pub fn identifier(&self) -> &Option<String> {
51 &self.identifier
52 }
53
54 pub fn major(&self) -> u8 {
55 self.major
56 }
57
58 pub fn minor(&self) -> u8 {
59 self.minor
60 }
61
62 pub fn patch(&self) -> u8 {
63 self.patch
64 }
65
66 pub fn str(&self) -> String {
67 if self.identifier.is_some() {
68 format!(
69 "{}.{}.{}-{}",
70 self.major,
71 self.minor,
72 self.patch,
73 self.identifier.as_ref().unwrap()
74 )
75 } else {
76 format!("{}.{}.{}", self.major, self.minor, self.patch)
77 }
78 }
79
80 fn from_git_tag(git_tag: &String) -> Self {
87 let number_captures: Vec<Captures> = NUMBERS.captures_iter(&git_tag).collect();
88
89 let semver = if git_tag.contains(RELEASE_CANDIDATE_MARKER) {
90 if let Some(rc_match) = SemVer::get_rc_match(&git_tag, &number_captures) {
91 let captures_without_rc: Vec<Captures> = number_captures
92 .into_iter()
93 .filter(|cap| cap.get(FIRST_GROUP).unwrap().ne(&rc_match))
94 .collect();
95 let mut semver = SemVer::create_semver_from_regex(&captures_without_rc);
96 let rc_marker = format!("rc{}", rc_match.as_str());
97
98 semver.identifier = Some(rc_marker);
99 semver
100 } else {
101 panic!("Git tag is not parsable!");
102 }
103 } else {
104 let mut semver = SemVer::create_semver_from_regex(&number_captures);
105
106 for marker in &*TAG_MARKERS {
107 if git_tag.contains(marker) {
108 semver.identifier = Some(marker.to_owned());
109 }
110 }
111
112 semver
113 };
114
115 semver
116 }
117
118 fn create_semver_from_regex(captures: &[Captures]) -> Self {
119 let mut numbers: Vec<u8> = Vec::with_capacity(3);
120
121 for cap in captures {
122 numbers.push((&cap[1]).parse().unwrap())
123 }
124
125 let numbers_len = numbers.len();
127 if numbers_len < 3 {
128 for _ in numbers_len..3 {
129 numbers.push(0);
130 }
131 }
132
133 SemVer::new(numbers[0], numbers[1], numbers[2], None)
134 }
135
136 fn get_rc_match<'a>(git_tag: &String, number_captures: &Vec<Captures<'a>>) -> Option<Match<'a>> {
137 for cap in number_captures.iter().skip(1) {
139 let version_number = &cap[FIRST_GROUP];
140 let rc_query = format!("{}{}", RELEASE_CANDIDATE_MARKER, version_number);
141 if git_tag.contains(&rc_query) {
142 return Some(cap.get(FIRST_GROUP).unwrap().clone());
144 }
145 }
146 None
147 }
148}
149
150impl Display for SemVer {
151 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
152 write!(f, "{}", self.str())
153 }
154}
155
156#[derive(Clone, Serialize, Deserialize, Debug)]
164pub struct Tag {
165 #[serde(alias = "value")]
167 str: String,
168 semver: SemVer,
169}
170
171impl Tag {
172 pub fn new<S: Into<String>>(git_tag: S) -> Self {
173 let value = git_tag.into();
174 let semver = SemVer::from_git_tag(&value);
175
176 Tag { str: value, semver }
177 }
178
179 pub fn semver(&self) -> &SemVer {
181 &self.semver
182 }
183
184 pub fn str(&self) -> &String {
186 &self.str
187 }
188}
189
190impl Default for Tag {
191 fn default() -> Self {
192 Tag::new("")
193 }
194}
195
196impl From<String> for Tag {
197 fn from(s: String) -> Self {
198 Tag::new(&s)
199 }
200}
201
202impl From<&str> for Tag {
203 fn from(s: &str) -> Self {
204 Tag::new(s)
205 }
206}
207
208impl From<Option<String>> for Tag {
209 fn from(opt: Option<String>) -> Self {
210 match opt {
211 Some(str) => Tag::new(str),
212 None => Tag::default(),
213 }
214 }
215}
216
217impl From<Option<&str>> for Tag {
218 fn from(opt: Option<&str>) -> Self {
219 match opt {
220 Some(str) => Tag::new(str),
221 None => Tag::default(),
222 }
223 }
224}
225
226impl AsRef<Path> for Tag {
227 fn as_ref(&self) -> &Path {
228 self.str.as_ref()
229 }
230}
231
232impl AsRef<str> for Tag {
233 fn as_ref(&self) -> &str {
234 self.str.as_ref()
235 }
236}
237
238impl Display for Tag {
239 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
240 write!(f, "{}", self.str)
241 }
242}
243
244impl PartialEq<Tag> for Tag {
245 fn eq(&self, other: &Tag) -> bool {
246 self.semver.eq(other.semver())
247 }
248}
249
250impl PartialOrd<Tag> for Tag {
251 fn partial_cmp(&self, other: &Tag) -> Option<Ordering> {
252 self.semver.partial_cmp(other.semver())
253 }
254}
255
256impl Ord for Tag {
257 fn cmp(&self, other: &Self) -> Ordering {
258 self.semver.cmp(other.semver())
259 }
260}
261
262impl Eq for Tag {}
263
264impl From<Tag> for String {
265 fn from(tag: Tag) -> Self {
266 String::from(tag.str())
267 }
268}
269
270impl Hash for Tag {
271 fn hash<H: Hasher>(&self, state: &mut H) {
272 self.str.hash(state)
273 }
274}
275
276#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
283#[serde(tag = "type")]
284pub enum TagKind {
285 Proton,
286 Wine { kind: WineTagKind },
287}
288
289impl TagKind {
290 pub fn wine() -> TagKind {
292 TagKind::Wine {
293 kind: WineTagKind::WineGe,
294 }
295 }
296
297 pub fn lol() -> TagKind {
299 TagKind::Wine {
300 kind: WineTagKind::LolWineGe,
301 }
302 }
303
304 pub fn values() -> Vec<TagKind> {
306 vec![TagKind::Proton, TagKind::wine(), TagKind::lol()]
307 }
308
309 pub fn compatibility_tool_name(&self) -> String {
311 let name = match self {
312 TagKind::Proton => "Proton GE",
313 TagKind::Wine { kind } => match kind {
314 WineTagKind::WineGe => "Wine GE",
315 WineTagKind::LolWineGe => "Wine GE (LoL)",
316 },
317 };
318 String::from(name)
319 }
320
321 pub fn compatibility_tool_kind(&self) -> String {
323 let name = match self {
324 TagKind::Proton => "Proton",
325 TagKind::Wine { .. } => "Wine",
326 };
327 String::from(name)
328 }
329
330 pub fn str(&self) -> String {
332 let name = match self {
333 TagKind::Proton => PROTON,
334 TagKind::Wine { kind } => match kind {
335 WineTagKind::WineGe => WINE,
336 WineTagKind::LolWineGe => LOL_WINE,
337 },
338 };
339 String::from(name)
340 }
341
342 fn from_str(str: &str) -> Result<Self, TagKindError> {
343 let kind = match str {
344 PROTON => TagKind::Proton,
345 WINE => TagKind::wine(),
346 LOL_WINE => TagKind::lol(),
347 _ => return Err(TagKindError::UnknownString),
348 };
349 Ok(kind)
350 }
351}
352
353impl From<&WineTagKind> for TagKind {
354 fn from(kind: &WineTagKind) -> Self {
355 TagKind::Wine { kind: *kind }
356 }
357}
358
359impl From<WineTagKind> for TagKind {
360 fn from(kind: WineTagKind) -> Self {
361 TagKind::Wine { kind }
362 }
363}
364
365impl TryFrom<&str> for TagKind {
366 type Error = TagKindError;
367
368 fn try_from(value: &str) -> Result<Self, Self::Error> {
369 TagKind::from_str(value)
370 }
371}
372
373impl TryFrom<String> for TagKind {
374 type Error = TagKindError;
375
376 fn try_from(value: String) -> Result<Self, Self::Error> {
377 TagKind::from_str(&value)
378 }
379}
380
381impl Display for TagKind {
382 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
383 write!(f, "{}", self.str())
384 }
385}
386
387#[derive(Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Debug)]
394#[serde(tag = "type")]
395pub enum WineTagKind {
396 WineGe,
397 LolWineGe,
398}
399
400impl From<&str> for WineTagKind {
401 fn from(string: &str) -> Self {
402 match string {
403 s if s.eq(WINE) => WineTagKind::WineGe,
404 s if s.eq(LOL_WINE) => WineTagKind::LolWineGe,
405 _ => panic!("Cannot map string to LutrisVersionKind"),
406 }
407 }
408}
409
410#[cfg(test)]
411mod tag_tests {
412 use test_case::test_case;
413
414 use super::*;
415
416 #[test_case("6.20-GE-1" => String::from("6.20.1"))]
417 #[test_case("6.20-GE-0" => String::from("6.20.0"))]
418 #[test_case("6.20-GE" => String::from("6.20.0"))]
419 #[test_case("6.16-GE-3-LoL" => String::from("6.16.3-LoL"))]
420 #[test_case("6.16-2-GE-LoL" => String::from("6.16.2-LoL"))]
421 #[test_case("6.16-GE-LoL" => String::from("6.16.0-LoL"))]
422 #[test_case("6.16-GE-0-LoL" => String::from("6.16.0-LoL"))]
423 #[test_case("6.16-0-GE-LoL" => String::from("6.16.0-LoL"))]
424 #[test_case("7.0rc3-GE-1" => String::from("7.0.1-rc3"))]
425 #[test_case("7.0rc3-GE-0" => String::from("7.0.0-rc3"))]
426 #[test_case("7.0rc3-GE" => String::from("7.0.0-rc3"))]
427 #[test_case("7.0-GE" => String::from("7.0.0"))]
428 #[test_case("7.0-GE-1" => String::from("7.0.1"))]
429 #[test_case("GE-Proton7-8" => String::from("7.8.0"))]
430 #[test_case("GE-Proton7-4" => String::from("7.4.0"))]
431 #[test_case("5.11-GE-1-MF" => String::from("5.11.1-MF"))]
432 #[test_case("proton-3.16-5" => String::from("3.16.5"))]
433 #[test_case("5.0-rc5-GE-1" => String::from("5.0.1-rc5"))]
434 fn get_semver_format(tag_str: &str) -> String {
435 let tag = Tag::new(tag_str);
436 tag.semver().to_string()
437 }
438
439 #[test]
440 fn create_from_json_before_release_0_2_0() {
441 let tag: Tag = serde_json::from_str(
442 r###"{
443 "value": "6.20-GE-1",
444 "semver": {
445 "major": 6, "minor": 20, "patch": 1, "identifier": null
446 }
447 }"###,
448 )
449 .unwrap();
450 assert_eq!(tag.str(), "6.20-GE-1");
451 }
452
453 #[test]
454 fn create_from_json() {
455 let tag: Tag = serde_json::from_str(
456 r###"{
457 "str": "6.20-GE-1",
458 "semver": {
459 "major": 6, "minor": 20, "patch": 1, "identifier": null
460 }
461 }"###,
462 )
463 .unwrap();
464 assert_eq!(tag.str(), "6.20-GE-1");
465 }
466
467 #[test_case(Tag::new("6.20-GE-1"), Tag::new("6.20-GE-1") => true)]
468 #[test_case(Tag::new("6.20-GE-1"), Tag::new("6.21-GE-1") => false)]
469 fn equality_tests(a: Tag, b: Tag) -> bool {
470 a.eq(&b)
471 }
472
473 #[test_case(Tag::new("6.20-GE-1"), Tag::new("6.20-GE-1") => Ordering::Equal)]
474 #[test_case(Tag::new("6.20-GE-1"), Tag::new("6.21-GE-1") => Ordering::Less)]
475 #[test_case(Tag::new("6.20-GE-1"), Tag::new("6.19-GE-1") => Ordering::Greater)]
476 #[test_case(Tag::new("GE-Proton7-8"), Tag::new("GE-Proton7-8") => Ordering::Equal)]
477 #[test_case(Tag::new("GE-Proton7-8"), Tag::new("GE-Proton7-20") => Ordering::Less)]
478 #[test_case(Tag::new("GE-Proton7-8"), Tag::new("GE-Proton7-7") => Ordering::Greater)]
479 fn comparison_tests(a: Tag, b: Tag) -> Ordering {
480 a.cmp(&b)
481 }
482}
483
484#[cfg(test)]
485mod tag_kind_tests {
486 use test_case::test_case;
487
488 use super::*;
489
490 #[test]
491 fn wine() {
492 let kind = TagKind::wine();
493 assert_eq!(
494 kind,
495 TagKind::Wine {
496 kind: WineTagKind::WineGe
497 }
498 )
499 }
500
501 #[test]
502 fn lol() {
503 let kind = TagKind::lol();
504 assert_eq!(
505 kind,
506 TagKind::Wine {
507 kind: WineTagKind::LolWineGe
508 }
509 );
510 }
511
512 #[test]
513 fn values() {
514 let values = TagKind::values();
515 assert_eq!(
516 values,
517 vec![
518 TagKind::Proton,
519 TagKind::Wine {
520 kind: WineTagKind::WineGe
521 },
522 TagKind::Wine {
523 kind: WineTagKind::LolWineGe
524 },
525 ]
526 );
527 }
528
529 #[test_case(TagKind::Proton => "Proton GE"; "Correct app name should be returned for Proton")]
530 #[test_case(TagKind::wine() => "Wine GE"; "Correct app name should be returned for Wine")]
531 #[test_case(TagKind::lol() => "Wine GE (LoL)"; "Correct app name should be returned for Wine (LoL)")]
532 fn get_compatibility_tool_name(kind: TagKind) -> String {
533 kind.compatibility_tool_name()
534 }
535
536 #[test_case(TagKind::Proton => "PROTON"; "Correct type name should be returned for Proton")]
537 #[test_case(TagKind::wine() => "WINE"; "Correct type name should be returned for Wine")]
538 #[test_case(TagKind::lol() => "LOL_WINE"; "Correct type name should be returned for Wine (LoL)")]
539 fn get_type_name(kind: TagKind) -> String {
540 kind.str()
541 }
542}