use crate::cmd::args::{read_arg, Arg};
use clap::{ArgMatches, Command};
use std::path::PathBuf;
#[derive(Default)]
pub(crate) struct MainCmd {
sub_cmds: Vec<Command>,
about: Option<&'static str>
}
impl From<&MainCmd> for Command {
fn from(value: &MainCmd) -> Self {
value.to_command(&[])
}
}
impl MainCmd {
pub fn about(mut self, about: &'static str) -> Self {
self.about = Some(about);
self
}
pub fn subcommand<T: Into<Command>>(mut self, command: T) -> Self {
self.sub_cmds.push(command.into());
self
}
pub fn to_command(&self, exclude: &[&str]) -> Command {
let mut cmd = Command::new("Odra CLI")
.subcommand_required(true)
.arg_required_else_help(true)
.arg(Arg::Contracts)
.arg(Arg::Json)
.subcommands(
self.sub_cmds
.iter()
.filter(|c| !exclude.contains(&c.get_name()))
.cloned()
);
if let Some(about) = self.about {
cmd = cmd.about(about);
}
cmd
}
pub fn get_matches(&self) -> (String, ArgMatches, Option<PathBuf>) {
match self.try_get_matches_from(std::env::args().collect()) {
Ok(result) => result,
Err(err) => err.exit()
}
}
pub fn try_get_matches_from(
&self,
argv: Vec<String>
) -> Result<(String, ArgMatches, Option<PathBuf>), clap::Error> {
self.try_get_matches_from_excluding(argv, &[])
}
pub fn try_get_matches_from_excluding(
&self,
argv: Vec<String>,
exclude: &[&str]
) -> Result<(String, ArgMatches, Option<PathBuf>), clap::Error> {
let clap_cmd = self.to_command(exclude);
let matches = clap_cmd.try_get_matches_from(argv)?;
let contracts_path = read_arg(&matches, Arg::Contracts);
let (subcommand, args) = matches
.subcommand()
.expect("subcommand is required and validated by clap");
Ok((subcommand.to_string(), args.clone(), contracts_path))
}
}
#[cfg(test)]
mod tests {
use clap::{command, error::ErrorKind};
use super::*;
#[test]
fn test_main_cmd_default() {
let main = MainCmd::default();
let main_cmd: Command = (&main).into();
assert_eq!(main_cmd.get_name(), "Odra CLI");
}
#[test]
fn test_main_cmd_requires_subcommand() {
let main = MainCmd::default();
let main_cmd: Command = (&main).into();
let result = main_cmd.try_get_matches_from(vec!["odra-cli"]);
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
}
#[test]
fn test_contracts_arg() {
let main = MainCmd::default().subcommand(command!("test"));
let main_cmd: Command = (&main).into();
let matches = main_cmd.get_matches_from(vec![
"odra-cli",
"--contracts-toml",
"path/to/contracts",
"test",
]);
let contracts_path = read_arg(&matches, Arg::Contracts);
assert_eq!(contracts_path, Some(PathBuf::from("path/to/contracts")));
}
#[test]
fn try_get_matches_from_returns_ok_for_valid_subcommand() {
let main = MainCmd::default().subcommand(command!("whoami"));
let (cmd, _args, path) = main
.try_get_matches_from(vec!["odra-cli".to_string(), "whoami".to_string()])
.expect("valid subcommand should parse");
assert_eq!(cmd, "whoami");
assert_eq!(path, None);
}
#[test]
fn try_get_matches_from_returns_err_for_unknown_subcommand() {
let main = MainCmd::default().subcommand(command!("whoami"));
let err = main
.try_get_matches_from(vec!["odra-cli".to_string(), "bogus".to_string()])
.expect_err("unknown subcommand should return an error, not exit");
assert_eq!(err.kind(), ErrorKind::InvalidSubcommand);
}
#[test]
fn try_get_matches_from_returns_err_for_help() {
let main = MainCmd::default().subcommand(command!("whoami"));
let err = main
.try_get_matches_from(vec!["odra-cli".to_string(), "--help".to_string()])
.expect_err("--help should return an error, not exit");
assert_eq!(err.kind(), ErrorKind::DisplayHelp);
}
}