ge-man 0.1.2

A manager for GE Proton and Wine GE versions.
Documentation
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use std::fs;
use std::path::Path;

use anyhow::{bail, Context};
use ge_man_lib::tag::{Tag, TagKind};
use serde::{Deserialize, Serialize};

use crate::version::{Version, Versioned};

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "type")]
pub struct ManagedVersion {
    tag: Tag,
    kind: TagKind,
    directory_name: String,
}

impl ManagedVersion {
    pub fn new<T, S>(tag: T, kind: TagKind, directory_name: S) -> Self
    where
        T: Into<Tag>,
        S: Into<String>,
    {
        let tag = tag.into();
        let directory_name = directory_name.into();
        ManagedVersion {
            tag,
            kind,
            directory_name,
        }
    }

    pub fn tag(&self) -> &Tag {
        &self.tag
    }

    pub fn set_tag<T: Into<Tag>>(&mut self, tag: T) {
        self.tag = tag.into();
    }

    pub fn kind(&self) -> &TagKind {
        &self.kind
    }

    pub fn set_kind(&mut self, kind: TagKind) {
        self.kind = kind;
    }

    pub fn directory_name(&self) -> &String {
        &self.directory_name
    }

    pub fn set_directory_name<S: Into<String>>(&mut self, name: S) {
        self.directory_name = name.into();
    }
}

impl From<Version> for ManagedVersion {
    fn from(v: Version) -> Self {
        let tag = v.tag().clone();
        let kind = *v.kind();
        ManagedVersion::new(tag, kind, String::new())
    }
}

impl From<&Version> for ManagedVersion {
    fn from(v: &Version) -> Self {
        let tag = v.tag().clone();
        let kind = *v.kind();
        ManagedVersion::new(tag, kind, String::new())
    }
}

impl Versioned for ManagedVersion {
    fn tag(&self) -> &Tag {
        &self.tag
    }

    fn kind(&self) -> &TagKind {
        &self.kind
    }
}

impl PartialEq for ManagedVersion {
    fn eq(&self, other: &Self) -> bool {
        self.tag().eq(other.tag()) && self.kind().eq(other.kind())
    }
}

impl<'a> PartialEq<dyn Versioned + 'a> for ManagedVersion {
    fn eq(&self, other: &(dyn Versioned + 'a)) -> bool {
        self.tag().eq(other.tag()) && self.kind().eq(other.kind())
    }
}

impl PartialOrd for ManagedVersion {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl<'a> PartialOrd<dyn Versioned + 'a> for ManagedVersion {
    fn partial_cmp(&self, other: &(dyn Versioned + 'a)) -> Option<Ordering> {
        Some(self.tag().cmp(other.tag()).then(self.kind().cmp(other.kind())))
    }
}

impl Eq for ManagedVersion {}

impl Ord for ManagedVersion {
    fn cmp(&self, other: &Self) -> Ordering {
        self.tag().cmp(other.tag()).then(self.kind().cmp(other.kind()))
    }
}

impl Display for ManagedVersion {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} ({})", self.tag, self.kind.compatibility_tool_kind())
    }
}

#[derive(Serialize, Deserialize)]
pub struct ManagedVersions {
    versions: Vec<ManagedVersion>,
}

impl ManagedVersions {
    pub fn new(items: Vec<ManagedVersion>) -> Self {
        ManagedVersions { versions: items }
    }

    pub fn from_file(path: &Path) -> anyhow::Result<Self> {
        let managed_versions = match fs::read_to_string(path) {
            Ok(json) => serde_json::from_str(&json)?,
            Err(err) => {
                if err.kind() == std::io::ErrorKind::NotFound {
                    ManagedVersions::default()
                } else {
                    bail!("Could not convert managed_versions.json to struct");
                }
            }
        };

        Ok(managed_versions)
    }

    pub fn write_to_file(&self, path: &Path) -> anyhow::Result<()> {
        let json = serde_json::to_string(&self).context("Could not convert managed version struct to json")?;

        fs::write(path, json).context("Could not write changes to managed_versions.json")?;

        Ok(())
    }

    fn get_version_index(&self, version: &dyn Versioned) -> Option<usize> {
        self.versions.iter().position(|i| i.eq(version))
    }

    pub fn find_latest_by_kind(&self, kind: &TagKind) -> Option<ManagedVersion> {
        self.versions
            .iter()
            .filter(|v| v.kind().eq(kind))
            .max_by(|a, b| a.tag().semver().cmp(&b.tag().semver()))
            .cloned()
    }

    pub fn add(&mut self, version: ManagedVersion) -> anyhow::Result<ManagedVersion> {
        self.versions.push(version.clone());

        Ok(version)
    }

    pub fn remove(&mut self, version: &dyn Versioned) -> Option<ManagedVersion> {
        match self.get_version_index(version) {
            Some(index) => Some(self.versions.swap_remove(index)),
            None => None,
        }
    }

    pub fn find_version(&self, version: &dyn Versioned) -> Option<ManagedVersion> {
        self.get_version_index(version)
            .and_then(|index| self.versions.get(index).cloned())
    }

    pub fn latest_versions(&self) -> Vec<ManagedVersion> {
        let kinds = TagKind::values();
        let mut versions = Vec::with_capacity(kinds.len());
        for kind in &kinds {
            if let Some(v) = self.find_latest_by_kind(kind) {
                versions.push(v);
            }
        }
        versions
    }

    #[allow(dead_code)]
    pub fn versions(&self) -> Vec<ManagedVersion> {
        self.versions.clone()
    }
}

impl Default for ManagedVersions {
    fn default() -> Self {
        ManagedVersions::new(Vec::new())
    }
}

#[cfg(test)]
mod managed_version_tests {
    use super::*;

    fn setup_version() -> ManagedVersion {
        ManagedVersion::new(Tag::from("6.20-GE-1"), TagKind::Proton, "Proton-6.20-GE-1")
    }

    #[test]
    fn get_tag() {
        let tag = "6.20-GE-1";
        let managed_version = setup_version();
        assert_eq!(managed_version.tag(), &Tag::from(tag));
    }

    #[test]
    fn set_tag() {
        let tag = "6.20-GE-1";
        let mut managed_version = setup_version();
        managed_version.set_tag("6.20-GE-1");
        assert_eq!(managed_version.tag(), &Tag::from(tag));
    }

    #[test]
    fn get_kind() {
        let managed_version = setup_version();
        assert_eq!(managed_version.kind(), &TagKind::Proton);
    }

    #[test]
    fn set_kind() {
        let mut managed_version = setup_version();
        managed_version.set_kind(TagKind::wine());
        assert_eq!(managed_version.kind(), &TagKind::wine());
    }

    #[test]
    fn get_directory_name() {
        let managed_version = setup_version();
        assert_eq!(managed_version.directory_name(), "Proton-6.20-GE-1");
    }

    #[test]
    fn set_directory_name() {
        let mut managed_version = setup_version();
        managed_version.set_directory_name("Test");
        assert_eq!(managed_version.directory_name(), "Test");
    }
}

#[cfg(test)]
mod managed_versions_tests {
    use std::fs::File;
    use std::io::Write;

    use assert_fs::TempDir;
    use ge_man_lib::tag::TagKind;
    use lazy_static::lazy_static;

    use super::*;

    lazy_static! {
        static ref VERSIONS: Vec<ManagedVersion> = vec![
            ManagedVersion::from(Version::proton("6.20-GE-1")),
            ManagedVersion::from(Version::proton("6.19-GE-2")),
            ManagedVersion::from(Version::wine("6.20-GE-1")),
            ManagedVersion::from(Version::wine("6.19-GE-2")),
            ManagedVersion::from(Version::lol("6.16-GE-3-LoL")),
            ManagedVersion::from(Version::lol("6.16-2-GE-Lol")),
        ];
    }

    #[test]
    fn latest_by_kind() {
        let managed_versions = ManagedVersions::new(VERSIONS.clone());

        let latest_proton = managed_versions.find_latest_by_kind(&TagKind::Proton).unwrap();
        let latest_wine = managed_versions.find_latest_by_kind(&TagKind::wine()).unwrap();
        let latest_lol = managed_versions.find_latest_by_kind(&TagKind::lol()).unwrap();
        assert_eq!(
            latest_proton,
            ManagedVersion::new(Tag::from("6.20-GE-1"), TagKind::Proton, String::new())
        );
        assert_eq!(
            latest_wine,
            ManagedVersion::new(Tag::from("6.20-GE-1"), TagKind::wine(), String::new())
        );
        assert_eq!(
            latest_lol,
            ManagedVersion::new(Tag::from("6.16-GE-3-LoL"), TagKind::lol(), String::new())
        );
    }

    #[test]
    fn latest_versions() {
        let managed_versions = ManagedVersions::new(VERSIONS.clone());
        let result = managed_versions.latest_versions();

        assert_eq!(
            result,
            vec![
                ManagedVersion::new(Tag::from("6.20-GE-1"), TagKind::Proton, String::new()),
                ManagedVersion::new(Tag::from("6.20-GE-1"), TagKind::wine(), String::new()),
                ManagedVersion::new(Tag::from("6.16-GE-3-LoL"), TagKind::lol(), String::new()),
            ]
        );
    }

    #[test]
    fn add() {
        let mut managed_versions = ManagedVersions::default();
        let version = ManagedVersion::from(Version::proton("6.20-GE-1"));
        managed_versions.add(version).unwrap();
        assert!(managed_versions.find_version(&Version::proton("6.20-GE-1")).is_some());
    }

    #[test]
    fn remove_existing_version() {
        let version = ManagedVersion::from(Version::proton("6.20-GE-1"));
        let mut managed_versions = ManagedVersions::new(vec![version]);
        managed_versions.remove(&Version::proton("6.20-GE-1")).unwrap();
        assert!(!managed_versions.find_version(&Version::proton("6.20-GE-1")).is_some());
        assert!(managed_versions.versions().is_empty());
    }

    #[test]
    fn remove_version_that_does_not_exist() {
        let mut managed_versions = ManagedVersions::default();
        let option = managed_versions.remove(&Version::proton("6.20-GE-1"));
        assert_eq!(option, None);
    }

    #[test]
    fn get_all_versions() {
        let managed_versions = ManagedVersions::new(VERSIONS.clone());
        assert_eq!(managed_versions.versions(), *VERSIONS);
        assert_eq!(managed_versions.versions().len(), 6);
    }

    #[test]
    fn has_version() {
        let version = ManagedVersion::from(Version::proton("6.19-GE-1"));
        let managed_versions = ManagedVersions::new(vec![version]);
        assert!(!managed_versions.find_version(&Version::proton("6.20-GE-1")).is_some());
        assert!(managed_versions.find_version(&Version::proton("6.19-GE-1")).is_some());
    }

    #[test]
    fn read_invalid_json() {
        let tmp_dir = TempDir::new().unwrap();
        let path = tmp_dir.join("version_mng.json");
        let mut file = File::create(&path).unwrap();
        file.write_all(b"{").unwrap();

        let result = ManagedVersions::from_file(&path);
        assert!(result.is_err());

        drop(file);
        tmp_dir.close().unwrap();
    }
}