ge_man_lib/
tag.rs

1//! Structs for representing a GitHub release tag.
2//!
3//! This module provides structs for working with GitHub release tags.
4
5use 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/// Struct used to contain semantic versioning information.
29///
30/// The primary use of this struct is to extract information from version strings in the GitHub assets and to provide
31/// it a as a semantic version.
32#[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    /// Create a `SemVer` type from a git tag.
81    ///
82    /// Some git tags contain special characters that need to be amended at the end of the semver string to stay
83    /// compliant with the semver standard. An example of this is the tag "5.0-rc5-GE-1", it should be represented as
84    /// "5.0.1-rc". At the moment, only the "rc" keyword has been observed in git tags and, therefore, only this keyword
85    /// is explicitly handled differently.
86    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        // In the case that we do not have enough matches to fill the semver string we fill it with empty zeros.
126        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        // Skip the first version number match because it might be the same number as the rc candidate.
138        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                // Since every match contains a single capture group, we always get the first capture group.
143                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/// Struct for representing a GitHub release tag and providing an option to be transformed into a semantic version.
157///
158/// Internally this struct parses the given release tag to create a semantic version representation from it. This is
159/// done because it is much easier to perform comparisons between `6.20.1` and `7.8.0` than with `Proton-6.20-GE-1` and
160/// `GE-Proton7-8`.
161///
162/// This struct supports `serde`'s serialization and deserialization traits.
163#[derive(Clone, Serialize, Deserialize, Debug)]
164pub struct Tag {
165    // Alias for versions before ge-man-lib version 0.2.0.
166    #[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    /// Get this `Tag` as a semantic version.
180    pub fn semver(&self) -> &SemVer {
181        &self.semver
182    }
183
184    /// Get the string value of this `Tag`.
185    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/// Represents the kind of version for a `Tag`.
277///
278/// GE versions exists for both Proton and Wine. Additionally, for Wine also League of Legends specific versions
279/// exist. Therefore, all possible version kinds are represented by this enum.
280///
281/// This enum supports `serde`'s serialization and deserialization traits.
282#[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    /// Create a Wine GE `TagKind`.
291    pub fn wine() -> TagKind {
292        TagKind::Wine {
293            kind: WineTagKind::WineGe,
294        }
295    }
296
297    /// Create a Wine GE LoL `TagKind`.
298    pub fn lol() -> TagKind {
299        TagKind::Wine {
300            kind: WineTagKind::LolWineGe,
301        }
302    }
303
304    /// Get all possible values.
305    pub fn values() -> Vec<TagKind> {
306        vec![TagKind::Proton, TagKind::wine(), TagKind::lol()]
307    }
308
309    /// Get a "human readable" compatibility tool name for the `TagKind`.
310    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    /// Get a "human readable" compatibility tool kind text for the `TagKind`.
322    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    /// Get a 1:1 string representation of the enum name.
331    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/// Represents a Wine GE or Wine GE LoL version.
388///
389/// Wine GE versions come in two flavours. One ist the normal Wine version with GE's patches and the other is a
390/// specific version for League of Legends.
391///
392/// This enum supports `serde`'s serialization and deserialization traits.
393#[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}