ge-man 0.1.2

A manager for GE Proton and Wine GE versions.
Documentation
use std::path::PathBuf;

use clap::ArgMatches;

use ge_man_lib::tag::{Tag, TagKind};

use crate::clap::{arg_group_names, arg_names, commands};
use crate::version::Version;

#[derive(Debug)]
pub struct TagArg {
    pub tag: Option<Tag>,
    pub kind: TagKind,
}

impl TagArg {
    pub fn new(tag: Option<Tag>, kind: TagKind) -> Self {
        let tag = tag.map(Into::into);
        TagArg { tag, kind }
    }

    pub fn value(&self) -> Option<&String> {
        self.tag.as_ref().map(Tag::value)
    }

    pub fn version(&self) -> Version {
        let tag = match &self.tag {
            Some(t) => t,
            None => panic!("TagArg tag value is None. Can not construct version."),
        };

        Version::new(tag.clone(), self.kind)
    }
}

impl TryFrom<&ArgMatches> for TagArg {
    type Error = ();

    fn try_from(matches: &ArgMatches) -> Result<Self, Self::Error> {
        let args: Vec<(&str, TagKind)> = vec![
            (arg_names::PROTON_ARG, TagKind::Proton),
            (arg_names::WINE_ARG, TagKind::wine()),
            (arg_names::LOL_ARG, TagKind::lol()),
        ];

        for (arg, kind) in args {
            if matches.is_present(arg) {
                if let Some(tag) = matches.value_of(arg) {
                    return Ok(TagArg::new(Some(Tag::from(tag)), kind));
                } else {
                    return Ok(TagArg::new(None, kind));
                }
            }
        }

        Err(())
    }
}

pub struct ListArgs {
    pub kind: Option<TagKind>,
    pub newest: bool,
}

impl ListArgs {
    pub fn new(kind: Option<TagKind>, newest: bool) -> Self {
        ListArgs { kind, newest }
    }
}

impl From<ArgMatches> for ListArgs {
    fn from(matches: ArgMatches) -> Self {
        let matches = matches.subcommand_matches(commands::LIST).unwrap();
        let newest = matches.is_present(arg_names::NEWEST_ARG);
        let kind = TagArg::try_from(matches).ok().map(|tag| tag.kind);

        ListArgs::new(kind, newest)
    }
}

#[derive(Debug)]
pub struct AddArgs {
    pub tag_arg: TagArg,
    pub skip_checksum: bool,
    pub apply: bool,
}

impl AddArgs {
    pub fn new(tag: TagArg, skip_checksum: bool, apply: bool) -> Self {
        AddArgs {
            tag_arg: tag,
            skip_checksum,
            apply,
        }
    }
}

impl From<ArgMatches> for AddArgs {
    fn from(matches: ArgMatches) -> Self {
        let matches = matches.subcommand_matches(commands::ADD).unwrap();
        let tag = TagArg::try_from(matches).expect("Could not create tag information from provided argument");
        let skip_checksum = matches.is_present(arg_names::SKIP_CHECKSUM_ARG);
        let apply = matches.is_present(arg_names::APPLY_ARG);

        AddArgs::new(tag, skip_checksum, apply)
    }
}

pub struct RemoveArgs {
    pub tag_arg: TagArg,
}

impl RemoveArgs {
    pub fn new(tag_arg: TagArg) -> Self {
        RemoveArgs { tag_arg }
    }
}

impl From<ArgMatches> for RemoveArgs {
    fn from(matches: ArgMatches) -> Self {
        let matches = matches.subcommand_matches(commands::REMOVE).unwrap();
        let tag_arg = TagArg::try_from(matches).expect("Could not create tag information from provided argument");
        if tag_arg.tag.is_none() {
            panic!("No version provided!")
        }

        RemoveArgs::new(tag_arg)
    }
}

pub struct CheckArgs {
    pub kind: Option<TagKind>,
}

impl CheckArgs {
    pub fn new(kind: Option<TagKind>) -> Self {
        CheckArgs { kind }
    }
}

impl From<ArgMatches> for CheckArgs {
    fn from(matches: ArgMatches) -> Self {
        let matches = matches.subcommand_matches(commands::CHECK).unwrap();
        if matches.is_present(arg_group_names::TAG) {
            let tag_arg = TagArg::try_from(matches).expect("Could not create tag information from provided argument");
            let kind = tag_arg.kind;
            CheckArgs::new(Some(kind))
        } else {
            CheckArgs::new(None)
        }
    }
}

impl Default for CheckArgs {
    fn default() -> Self {
        CheckArgs::new(None)
    }
}

pub struct MigrationArgs {
    pub tag_arg: TagArg,
    pub source_path: PathBuf,
}

impl MigrationArgs {
    pub fn new<P: Into<PathBuf>>(tag_arg: TagArg, source_path: P) -> Self {
        let source_path = source_path.into();
        MigrationArgs { tag_arg, source_path }
    }
}

impl From<ArgMatches> for MigrationArgs {
    fn from(matches: ArgMatches) -> Self {
        let matches = matches.subcommand_matches(commands::MIGRATE).unwrap();
        let tag_arg = TagArg::try_from(matches).expect("Could not create tag information from provided argument");
        let source_path = matches.value_of(arg_names::SOURCE_ARG).unwrap();

        MigrationArgs::new(tag_arg, PathBuf::from(source_path))
    }
}

pub struct ApplyArgs {
    pub tag_arg: TagArg,
}

impl ApplyArgs {
    pub fn new(tag_arg: TagArg) -> Self {
        ApplyArgs { tag_arg }
    }
}

impl From<ArgMatches> for ApplyArgs {
    fn from(matches: ArgMatches) -> Self {
        let matches = matches.subcommand_matches(commands::APPLY).unwrap();
        let tag_arg = TagArg::try_from(matches).expect("Could not create tag information from provided argument");
        ApplyArgs::new(tag_arg)
    }
}

pub struct CopyUserSettingsArgs {
    pub src_tag: Tag,
    pub dst_tag: Tag,
}

impl CopyUserSettingsArgs {
    pub fn new<T: Into<Tag>>(src_tag: T, dst_tag: T) -> Self {
        let src_tag = src_tag.into();
        let dst_tag = dst_tag.into();
        CopyUserSettingsArgs { src_tag, dst_tag }
    }
}

impl Default for CopyUserSettingsArgs {
    fn default() -> Self {
        CopyUserSettingsArgs::new("", "")
    }
}

impl From<ArgMatches> for CopyUserSettingsArgs {
    fn from(matches: ArgMatches) -> Self {
        let matches = matches.subcommand_matches(commands::PROTON_USER_SETTINGS).unwrap();
        let matches = matches.subcommand_matches(commands::USER_SETTINGS_COPY).unwrap();
        let src_tag = matches.value_of(arg_names::SOURCE_ARG).unwrap();
        let dst_tag = matches.value_of(arg_names::DESTINATION_ARG).unwrap();

        CopyUserSettingsArgs::new(src_tag, dst_tag)
    }
}

pub struct ForgetArgs {
    pub tag_arg: TagArg,
}

impl ForgetArgs {
    pub fn new(tag_arg: TagArg) -> Self {
        ForgetArgs { tag_arg }
    }
}

impl From<ArgMatches> for ForgetArgs {
    fn from(matches: ArgMatches) -> Self {
        let matches = matches.subcommand_matches(commands::FORGET).unwrap();
        let tag_arg = TagArg::try_from(matches).expect("Could not create tag information from provided argument");

        ForgetArgs::new(tag_arg)
    }
}

#[cfg(test)]
mod tests {
    use clap::ErrorKind;
    use test_case::test_case;

    use crate::clap::setup_clap;

    use super::*;

    fn assert_tag_arg(tag_arg: TagArg, expected: TagArg) {
        assert_eq!(tag_arg.tag, expected.tag);
        assert_eq!(tag_arg.kind, expected.kind);
    }

    fn kind_str_to_enum(kind: &str) -> TagKind {
        match kind {
            "-p" => TagKind::Proton,
            "-w" => TagKind::wine(),
            "-l" => TagKind::lol(),
            _ => panic!("Test setup failed: Unexpected kind"),
        }
    }

    fn add_test_template(args: Vec<&str>, expected: AddArgs) {
        let matches = setup_clap().try_get_matches_from(args).unwrap();
        let args = AddArgs::from(matches);

        assert_tag_arg(args.tag_arg, expected.tag_arg);
        assert_eq!(args.skip_checksum, expected.skip_checksum);
        assert_eq!(args.apply, expected.apply);
    }

    fn remove_test_template(args: Vec<&str>, expected: RemoveArgs) {
        let matches = setup_clap().try_get_matches_from(args).unwrap();
        let args = RemoveArgs::from(matches);

        assert_tag_arg(args.tag_arg, expected.tag_arg);
    }

    fn check_test_template(args: Vec<&str>, expected: CheckArgs) {
        let matches = setup_clap().try_get_matches_from(args).unwrap();
        let args = CheckArgs::from(matches);

        assert_eq!(args.kind, expected.kind);
    }

    fn migration_test_template(args: Vec<&str>, expected: MigrationArgs) {
        let matches = setup_clap().try_get_matches_from(args).unwrap();
        let args = MigrationArgs::from(matches);

        assert_tag_arg(args.tag_arg, expected.tag_arg);
        assert_eq!(args.source_path, expected.source_path);
    }

    fn apply_test_template(args: Vec<&str>, expected: ApplyArgs) {
        let matches = setup_clap().try_get_matches_from(args).unwrap();
        let args = ApplyArgs::from(matches);

        assert_tag_arg(args.tag_arg, expected.tag_arg);
    }

    fn copy_user_settings_test_template(args: Vec<&str>, expected: CopyUserSettingsArgs) {
        let matches = setup_clap().try_get_matches_from(args).unwrap();
        let args = CopyUserSettingsArgs::from(matches);

        assert_eq!(args.src_tag, expected.src_tag);
        assert_eq!(args.dst_tag, expected.dst_tag);
    }

    fn forget_test_template(args: Vec<&str>, expected: ForgetArgs) {
        let matches = setup_clap().try_get_matches_from(args).unwrap();
        let args = ForgetArgs::from(matches);

        assert_tag_arg(args.tag_arg, expected.tag_arg);
    }

    fn list_test_template(args: Vec<&str>, expected: ListArgs) {
        let matches = setup_clap().try_get_matches_from(args).unwrap();
        let args = ListArgs::from(matches);

        assert_eq!(args.kind, expected.kind);
        assert_eq!(args.newest, expected.newest);
    }

    #[test_case("-p"; "Add specific Proton GE version")]
    #[test_case("-w"; "Add specific Wine GE version")]
    #[test_case("-l"; "Add specific Wine GE LoL version")]
    fn add_specific_proton_tag(kind: &str) {
        let args = vec!["geman", "add", kind, "6.20-GE-1"];
        let expected = AddArgs::new(
            TagArg::new(Some(Tag::from("6.20-GE-1")), kind_str_to_enum(kind)),
            false,
            false,
        );
        add_test_template(args, expected);
    }

    #[test_case("-p"; "Add latest Proton GE version")]
    #[test_case("-w"; "Add latest Wine GE version")]
    #[test_case("-l"; "Add latest Wine GE LoL version")]
    fn add_latest_tag(kind: &str) {
        let args = vec!["geman", "add", kind];
        let expected = AddArgs::new(TagArg::new(None, kind_str_to_enum(kind)), false, false);
        add_test_template(args, expected);
    }

    #[test]
    fn add_only_one_tag_arg_allowed() {
        let args = vec!["geman", "add", "-p", "6.20-GE-1", "-w", "6.20-GE-1"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::ArgumentConflict);
    }

    #[test]
    fn add_with_checksum_skip() {
        let args = vec!["geman", "add", "-p", "--skip-checksum"];
        let expected = AddArgs::new(TagArg::new(None, TagKind::Proton), true, false);
        add_test_template(args, expected);
    }

    #[test]
    fn add_with_apply() {
        let args = vec!["geman", "add", "-p", "--apply"];
        let expected = AddArgs::new(TagArg::new(None, TagKind::Proton), false, true);
        add_test_template(args, expected);
    }

    #[test]
    fn add_should_require_one_tag_arg() {
        let args = vec!["geman", "add"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
    }

    #[test_case("-p"; "Remove Proton GE version")]
    #[test_case("-w"; "Remove Wine GE version")]
    #[test_case("-l"; "Remove Wine GE LoL version")]
    fn remove_specific_tag(kind: &str) {
        let args = vec!["geman", "rm", kind, "6.20-GE-1"];
        let expected = RemoveArgs::new(TagArg::new(Some(Tag::from("6.20-GE-1")), kind_str_to_enum(kind)));
        remove_test_template(args, expected);
    }

    #[test_case("-p"; "Remove Proton GE version")]
    #[test_case("-w"; "Remove Wine GE version")]
    #[test_case("-l"; "Remove Wine GE LoL version")]
    fn remove_should_require_a_tag_argument_with_a_value(kind: &str) {
        let args = vec!["geman", "rm", kind];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::EmptyValue);
    }

    #[test]
    fn remove_only_one_tag_arg_allowed() {
        let args = vec!["geman", "rm", "-p", "6.20-GE-1", "-w", "6.20-GE-1"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::ArgumentConflict);
    }

    #[test]
    fn remove_should_require_a_tag_argument() {
        let args = vec!["geman", "rm"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
    }

    #[test]
    fn check_without_args_should_not_throw_error() {
        let args = vec!["geman", "check"];
        check_test_template(args, CheckArgs::default());
    }

    #[test_case("-p"; "Check for Proton GE")]
    #[test_case("-w"; "Check for Wine GE")]
    #[test_case("-l"; "Check for Wine GE LoL")]
    fn check_with_tag_kind(kind: &str) {
        let args = vec!["geman", "check", kind];
        let expected = CheckArgs::new(Some(kind_str_to_enum(kind)));
        check_test_template(args, expected);
    }

    #[test]
    fn check_only_one_tag_arg_allowed() {
        let args = vec!["geman", "check", "-p", "-w"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::ArgumentConflict);
    }

    #[test]
    fn migrate_tag_arg_is_required() {
        let args = vec!["geman", "migrate", "-s", "/tmp"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
    }

    #[test_case("-p"; "Migration for Proton GE")]
    #[test_case("-w"; "Migration for Wine GE")]
    #[test_case("-l"; "Migration for Wine GE LoL")]
    fn migrate_source_path_is_required(kind: &str) {
        let args = vec!["geman", "migrate", kind, "6.20-GE-1"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
    }

    #[test]
    fn migrate_with_all_required_args() {
        let args = vec!["geman", "migrate", "-p", "6.20-GE-1", "-s", "/tmp"];
        let expected = MigrationArgs::new(
            TagArg::new(Some(Tag::from("6.20-GE-1")), TagKind::Proton),
            PathBuf::from("/tmp"),
        );
        migration_test_template(args, expected);
    }

    #[test]
    fn migrate_only_one_tag_arg_allowed() {
        let args = vec![
            "geman",
            "migrate",
            "-s",
            "/tmp",
            "-p",
            "6.20-GE-1",
            "-w",
            "6.20-GE-1",
        ];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::ArgumentConflict);
    }

    #[test_case("-p"; "Apply for Proton GE")]
    #[test_case("-w"; "Apply for Wine GE")]
    #[test_case("-l"; "Apply for Wine GE LoL")]
    fn apply_with_all_required_args(kind: &str) {
        let args = vec!["geman", "apply", kind, "6.20-GE-1"];
        let expected = ApplyArgs::new(TagArg::new(Some(Tag::from("6.20-GE-1")), kind_str_to_enum(kind)));
        apply_test_template(args, expected);
    }

    #[test]
    fn apply_missing_required_tag_arg() {
        let args = vec!["geman", "apply"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
    }

    #[test_case("-p"; "Apply for Proton GE")]
    #[test_case("-w"; "Apply for Wine GE")]
    #[test_case("-l"; "Apply for Wine GE LoL")]
    fn apply_missing_value_for_tag_arg(kind: &str) {
        let args = vec!["geman", "apply", kind];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::EmptyValue);
    }

    #[test]
    fn copy_user_settings_with_all_required_args() {
        let args = vec![
            "geman",
            "user-settings",
            "copy",
            "-s",
            "6.20-GE-1",
            "-d",
            "6.21-GE-1",
        ];
        let mut expected = CopyUserSettingsArgs::default();
        expected.src_tag = Tag::from("6.20-GE-1");
        expected.dst_tag = Tag::from("6.21-GE-1");
        copy_user_settings_test_template(args, expected);
    }

    #[test]
    fn copy_user_settings_should_fail_without_destination() {
        let args = vec!["geman", "user-settings", "copy", "-s", "6.20-GE-1"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
    }

    #[test]
    fn copy_user_settings_should_fail_without_source() {
        let args = vec!["geman", "user-settings", "copy", "-d", "6.21-GE-1"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
    }

    #[test_case("-p"; "Forget a Proton GE version")]
    #[test_case("-w"; "Forget a Wine GE version")]
    #[test_case("-l"; "Forget a Wine GE LoL version")]
    fn forget_without_tag(kind: &str) {
        let args = vec!["geman", "forget", kind];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::EmptyValue);
    }

    #[test]
    fn forget_without_kind() {
        let args = vec!["geman", "forget"];
        let result = setup_clap().try_get_matches_from(args);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
    }

    #[test]
    fn forget_with_all_required_args() {
        let args = vec!["geman", "forget", "-p", "6.20-GE-1"];
        let expected = ForgetArgs::new(TagArg::new(Some(Tag::from("6.20-GE-1")), TagKind::Proton));
        forget_test_template(args, expected);
    }

    #[test_case("-p"; "Forget a Proton GE version")]
    #[test_case("-w"; "Forget a Wine GE version")]
    #[test_case("-l"; "Forget a Wine GE LoL version")]
    fn list_with_kind_filters(kind: &str) {
        let args = vec!["geman", "list", kind];
        let expected = ListArgs::new(Some(kind_str_to_enum(kind)), false);
        list_test_template(args, expected);
    }

    #[test]
    fn list_with_no_args() {
        let args = vec!["geman", "list"];
        let expected = ListArgs::new(None, false);
        list_test_template(args, expected);
    }

    #[test]
    fn list_with_latest() {
        let args = vec!["geman", "list", "-n"];
        let expected = ListArgs::new(None, true);
        list_test_template(args, expected);
    }
}