use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct AppCli {
#[arg(long, global = true)]
pub config: Option<PathBuf>,
#[arg(long, global = true, conflicts_with = "no_profile")]
pub profile: Option<String>,
#[arg(long, global = true)]
pub no_profile: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
Update(CommandArgs),
Search(SearchArgs),
List(ListArgs),
Install(InstallArgs),
Uninstall(UninstallArgs),
Enable(ToggleArgs),
Disable(ToggleArgs),
Migrate,
Profile(ProfileArgs),
Launch(LaunchArgs),
Export(ExportArgs),
Import(ImportArgs),
}
#[derive(Args)]
pub struct InstallArgs {
#[arg(required = true)]
pub mods: Vec<String>,
#[arg(long)]
pub force: bool,
}
#[derive(Args)]
pub struct UninstallArgs {
#[arg(required_unless_present = "all")]
pub mods: Vec<String>,
#[arg(long, conflicts_with = "mods")]
pub all: bool,
#[arg(long, conflicts_with = "mods")]
pub yes: bool,
#[arg(long)]
pub force: bool,
}
#[derive(Args)]
pub struct ToggleArgs {
#[arg(required_unless_present = "all")]
pub name: Option<String>,
#[arg(long, conflicts_with = "name")]
pub all: bool,
}
#[derive(Args)]
pub struct ListArgs {
#[arg(long, value_enum, default_value_t = ListFormat::Text)]
pub format: ListFormat,
}
#[derive(Clone, ValueEnum)]
pub enum ListFormat {
Text,
Json,
}
#[derive(Args)]
pub struct CommandArgs {
#[command(subcommand)]
pub command: UpdatesCommand,
}
#[derive(Args)]
pub struct SearchArgs {
pub term: String,
}
#[derive(Subcommand)]
pub enum UpdatesCommand {
Manifest,
Mods(UpdateModsArgs),
}
#[derive(Args)]
pub struct UpdateModsArgs {
#[arg(long)]
pub force: bool,
}
#[derive(Args)]
pub struct ProfileArgs {
#[command(subcommand)]
pub command: ProfileCommand,
}
#[derive(Subcommand)]
pub enum ProfileCommand {
List,
Create {
name: String,
},
Delete {
name: String,
},
Rename {
from: String,
to: String,
},
Duplicate {
from: String,
to: String,
},
Use {
name: String,
},
Clear,
}
#[derive(Args)]
pub struct LaunchArgs {
#[arg(long)]
pub vanilla: bool,
#[arg(long)]
pub print_steam_options: bool,
#[arg(last = true)]
pub args: Vec<String>,
}
#[derive(Args)]
pub struct ExportArgs {
#[arg(long)]
pub code: bool,
}
#[derive(Args)]
pub struct ImportArgs {
pub source: String,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn test_command_search() {
let app = AppCli::command();
let search_command = app.find_subcommand("search").unwrap();
assert_eq!(search_command.get_name(), "search");
assert!(search_command.get_about().is_some());
assert!(
search_command
.get_about()
.unwrap()
.to_string()
.contains("Search for mods by name")
);
let search_args = search_command.get_arguments().collect::<Vec<_>>();
assert_eq!(search_args.len(), 1);
assert_eq!(search_args[0].get_id().as_str(), "term");
assert!(search_args[0].get_help().is_some());
assert!(
search_args[0]
.get_help()
.unwrap()
.to_string()
.contains("Search term to find mods by name")
);
}
#[test]
fn test_command_update() {
let app = AppCli::command();
let update_command = app.find_subcommand("update").unwrap();
assert_eq!(update_command.get_name(), "update");
assert!(update_command.get_about().is_some());
assert!(
update_command
.get_about()
.unwrap()
.to_string()
.contains("Update the mod manifest or installed mods")
);
let update_subcommands = update_command.get_subcommands().collect::<Vec<_>>();
assert_eq!(update_subcommands.len(), 2);
let manifest_cmd = update_subcommands
.iter()
.find(|cmd| cmd.get_name() == "manifest")
.unwrap();
let mods_cmd = update_subcommands
.iter()
.find(|cmd| cmd.get_name() == "mods")
.unwrap();
assert!(manifest_cmd.get_about().is_some());
assert!(
manifest_cmd
.get_about()
.unwrap()
.to_string()
.contains("Update the mod manifest from the server")
);
assert!(mods_cmd.get_about().is_some());
assert!(
mods_cmd
.get_about()
.unwrap()
.to_string()
.contains("Update installed mods to their latest versions")
);
}
#[test]
fn profile_flags_are_global_and_mutually_exclusive() {
let app = AppCli::command();
let profile = app
.get_arguments()
.find(|a| a.get_id().as_str() == "profile")
.expect("--profile should be a global argument");
assert!(profile.is_global_set());
let no_profile = app
.get_arguments()
.find(|a| a.get_id().as_str() == "no_profile")
.expect("--no-profile should be a global argument");
assert!(no_profile.is_global_set());
let conflict = AppCli::try_parse_from([
"vmm",
"--profile",
"experiment",
"--no-profile",
"search",
"jotunn",
]);
assert!(conflict.is_err());
}
#[test]
fn uninstall_takes_mods_and_an_opt_in_force_flag() {
let app = AppCli::command();
let uninstall = app.find_subcommand("uninstall").unwrap();
let force = uninstall
.get_arguments()
.find(|arg| arg.get_id().as_str() == "force")
.expect("uninstall should offer --force");
assert!(force.get_help().is_some());
let parsed = AppCli::try_parse_from(["vmm", "uninstall", "Owner-Mod"]).unwrap();
match parsed.command {
Command::Uninstall(args) => {
assert_eq!(args.mods, vec!["Owner-Mod".to_string()]);
assert!(!args.force);
}
_ => panic!("expected the uninstall subcommand"),
}
let forced = AppCli::try_parse_from(["vmm", "uninstall", "--force", "Owner-Mod"]).unwrap();
match forced.command {
Command::Uninstall(args) => assert!(args.force),
_ => panic!("expected the uninstall subcommand"),
}
}
#[test]
fn install_takes_mods_and_an_opt_in_force_flag() {
let app = AppCli::command();
let install = app.find_subcommand("install").unwrap();
let force = install
.get_arguments()
.find(|arg| arg.get_id().as_str() == "force")
.expect("install should offer --force");
assert!(force.get_help().is_some());
assert!(force.get_short().is_none());
let parsed = AppCli::try_parse_from(["vmm", "install", "Owner-Mod"]).unwrap();
match parsed.command {
Command::Install(args) => {
assert_eq!(args.mods, vec!["Owner-Mod".to_string()]);
assert!(!args.force);
}
_ => panic!("expected the install subcommand"),
}
let forced = AppCli::try_parse_from(["vmm", "install", "--force", "Owner-Mod"]).unwrap();
match forced.command {
Command::Install(args) => {
assert_eq!(args.mods, vec!["Owner-Mod".to_string()]);
assert!(args.force);
}
_ => panic!("expected the install subcommand"),
}
}
#[test]
fn update_mods_takes_an_opt_in_force_flag() {
let parsed = AppCli::try_parse_from(["vmm", "update", "mods"]).unwrap();
match parsed.command {
Command::Update(sub) => match sub.command {
UpdatesCommand::Mods(args) => assert!(!args.force),
UpdatesCommand::Manifest => panic!("expected the mods subcommand"),
},
_ => panic!("expected the update subcommand"),
}
let forced = AppCli::try_parse_from(["vmm", "update", "mods", "--force"]).unwrap();
match forced.command {
Command::Update(sub) => match sub.command {
UpdatesCommand::Mods(args) => assert!(args.force),
UpdatesCommand::Manifest => panic!("expected the mods subcommand"),
},
_ => panic!("expected the update subcommand"),
}
}
#[test]
fn uninstall_takes_either_mods_or_all_and_an_opt_in_yes() {
let all = AppCli::try_parse_from(["vmm", "uninstall", "--all", "--yes"]).unwrap();
match all.command {
Command::Uninstall(args) => {
assert!(args.all);
assert!(args.yes);
assert!(args.mods.is_empty());
}
_ => panic!("expected the uninstall subcommand"),
}
let prompted = AppCli::try_parse_from(["vmm", "uninstall", "--all"]).unwrap();
match prompted.command {
Command::Uninstall(args) => assert!(!args.yes),
_ => panic!("expected the uninstall subcommand"),
}
assert!(AppCli::try_parse_from(["vmm", "uninstall", "--all", "Owner-Mod"]).is_err());
assert!(AppCli::try_parse_from(["vmm", "uninstall"]).is_err());
assert!(AppCli::try_parse_from(["vmm", "uninstall", "--yes", "Owner-Mod"]).is_err());
}
#[test]
fn test_command_list() {
let app = AppCli::command();
let list_command = app.find_subcommand("list").unwrap();
assert_eq!(list_command.get_name(), "list");
assert!(list_command.get_about().is_some());
assert!(
list_command
.get_about()
.unwrap()
.to_string()
.contains("List installed mods")
);
let list_args = list_command.get_arguments().collect::<Vec<_>>();
let format_arg = list_args
.iter()
.find(|a| a.get_id().as_str() == "format")
.unwrap();
assert!(format_arg.get_default_values().iter().any(|v| v == "text"));
}
#[test]
fn toggle_takes_either_a_mod_name_or_all_but_not_both() {
let named = AppCli::try_parse_from(["vmm", "disable", "Owner-Mod"]).unwrap();
match named.command {
Command::Disable(args) => {
assert_eq!(args.name.as_deref(), Some("Owner-Mod"));
assert!(!args.all);
}
_ => panic!("expected the disable subcommand"),
}
let all = AppCli::try_parse_from(["vmm", "enable", "--all"]).unwrap();
match all.command {
Command::Enable(args) => {
assert!(args.all);
assert!(args.name.is_none());
}
_ => panic!("expected the enable subcommand"),
}
assert!(AppCli::try_parse_from(["vmm", "enable", "--all", "Owner-Mod"]).is_err());
assert!(AppCli::try_parse_from(["vmm", "enable"]).is_err());
}
}