1use rootcause::report;
2use ytil_sys::cli::Args;
3
4pub mod ci;
5pub mod local;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum Help {
9 Root,
10 Ci,
11 CiAll,
12 CiLint,
13 CiTest,
14 CiReleaseNative,
15 CiAudit,
16}
17
18impl Help {
19 pub(crate) fn from_args(args: &[String]) -> Self {
20 if args.first().is_none_or(|arg| arg != "ci") {
21 return Self::Root;
22 }
23
24 match args.get(1).map(String::as_str) {
25 Some("all") => Self::CiAll,
26 Some("lint") => Self::CiLint,
27 Some("test") => Self::CiTest,
28 Some("release-native") => Self::CiReleaseNative,
29 Some("audit") => Self::CiAudit,
30 _ => Self::Ci,
31 }
32 }
33
34 pub const fn text(self) -> &'static str {
35 match self {
36 Self::Root => include_str!("../help.txt"),
37 Self::Ci => include_str!("../help/ci/help.txt"),
38 Self::CiAll => include_str!("../help/ci/all/help.txt"),
39 Self::CiLint => include_str!("../help/ci/lint/help.txt"),
40 Self::CiTest => include_str!("../help/ci/test/help.txt"),
41 Self::CiReleaseNative => include_str!("../help/ci/release-native/help.txt"),
42 Self::CiAudit => include_str!("../help/ci/audit/help.txt"),
43 }
44 }
45}
46
47#[derive(Debug, Eq, PartialEq)]
48pub enum Cmd {
49 Help(Help),
50 Ci(ci::CmdKind),
51 Local(Vec<String>),
52}
53
54impl Cmd {
55 pub fn from_env() -> rootcause::Result<Self> {
56 let args = ytil_sys::cli::get();
57 let help = Help::from_args(&args);
58 Self::try_from(args).inspect_err(|_| eprintln!("{}", help.text()))
59 }
60}
61
62impl TryFrom<Vec<String>> for Cmd {
63 type Error = rootcause::Report;
64
65 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
66 if args.has_help() {
67 return Ok(Self::Help(Help::from_args(&args)));
68 }
69
70 if args.first().is_some_and(|arg| arg == "ci") {
71 let command = ci::cmd_from_args(&args)?.ok_or_else(|| report!("missing evoke ci command"))?;
72 return Ok(Self::Ci(command));
73 }
74
75 Ok(Self::Local(args))
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use test_that::prelude::*;
82
83 use super::*;
84
85 #[rstest::rstest]
86 #[case::help(vec!["--help"], Cmd::Help(Help::Root))]
87 #[case::local(vec!["--debug"], Cmd::Local(vec!["--debug".to_owned()]))]
88 #[case::ci_default(vec!["ci"], Cmd::Ci(ci::CmdKind::All))]
89 #[case::ci_lint(vec!["ci", "lint"], Cmd::Ci(ci::CmdKind::Lint))]
90 fn test_parse_known_commands_returns_expected_command(#[case] args: Vec<&str>, #[case] expected: Cmd) {
91 assert_that!(parse(args), ok(eq(expected)));
92 }
93
94 #[rstest::rstest]
95 #[case::ci_extra_arg(vec!["ci", "lint", "extra"])]
96 #[case::ci_unknown_command(vec!["ci", "unknown"])]
97 fn test_parse_invalid_ci_commands_returns_error(#[case] args: Vec<&str>) {
98 assert_that!(parse(args), err(anything()));
99 }
100
101 #[rstest::rstest]
102 #[case::root(vec!["--help"], Help::Root)]
103 #[case::ci(vec!["ci", "--help"], Help::Ci)]
104 #[case::ci_lint(vec!["ci", "lint", "--help"], Help::CiLint)]
105 fn test_help_when_command_path_varies_selects_the_deepest_command(#[case] args: Vec<&str>, #[case] expected: Help) {
106 let args = args.into_iter().map(ToOwned::to_owned).collect::<Vec<_>>();
107 assert_that!(Help::from_args(&args), eq(expected));
108 }
109
110 fn parse(args: Vec<&str>) -> rootcause::Result<Cmd> {
111 Cmd::try_from(args.into_iter().map(ToOwned::to_owned).collect::<Vec<String>>())
112 }
113}