1use std::ffi::OsString;
2
3use rootcause::report;
4use ytil_sys::pico_args::Arguments;
5
6pub mod repo;
7pub mod rsl;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum Help {
11 Root,
12 Repo,
13 RepoFix,
14 Rsl,
15}
16
17impl Help {
18 pub(crate) fn from_args(args: &[OsString]) -> Self {
19 match args.first().map(|arg| arg.to_string_lossy()).as_deref() {
20 Some("repo") if args.get(1).is_some_and(|arg| arg == "fix") => Self::RepoFix,
21 Some("repo") => Self::Repo,
22 Some("rsl") => Self::Rsl,
23 _ => Self::Root,
24 }
25 }
26
27 pub const fn text(self) -> &'static str {
28 match self {
29 Self::Root => include_str!("../help.txt"),
30 Self::Repo => include_str!("../help/repo/help.txt"),
31 Self::RepoFix => include_str!("../help/repo/fix/help.txt"),
32 Self::Rsl => include_str!("../help/rsl/help.txt"),
33 }
34 }
35}
36
37#[derive(Debug, Eq, PartialEq)]
38pub enum Cmd {
39 Help(Help),
40 Repo(Vec<OsString>),
41 Rsl(Vec<OsString>),
42}
43
44impl Cmd {
45 pub fn from_env() -> rootcause::Result<Self> {
46 let args = Arguments::from_env();
47 let help = Help::from_args(&args.clone().finish());
48 Self::try_from(args).inspect_err(|_| eprintln!("{}", help.text()))
49 }
50}
51
52impl TryFrom<Arguments> for Cmd {
53 type Error = rootcause::Report;
54
55 fn try_from(mut args: Arguments) -> Result<Self, Self::Error> {
56 let Some(command) = args.subcommand()? else {
57 return if args.contains("--help") || args.finish().is_empty() {
58 Ok(Self::Help(Help::Root))
59 } else {
60 Err(report!("unsupported frs command"))
61 };
62 };
63
64 if args.clone().contains("--help") {
65 let raw_args = args.clone().finish();
66 let full_args = std::iter::once(OsString::from(command.as_str()))
67 .chain(raw_args)
68 .collect::<Vec<_>>();
69 return Ok(Self::Help(Help::from_args(&full_args)));
70 }
71
72 let remaining = args.finish();
73 match command.as_str() {
74 "repo" => Ok(Self::Repo(remaining)),
75 "rsl" => Ok(Self::Rsl(remaining)),
76 command => Err(report!("unsupported frs command").attach(format!("command={command}"))),
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use std::ffi::OsString;
84
85 use test_that::prelude::*;
86 use ytil_sys::pico_args::Arguments;
87
88 use super::*;
89
90 #[rstest::rstest]
91 #[case::bare(&[], Cmd::Help(Help::Root))]
92 #[case::help(&["--help"], Cmd::Help(Help::Root))]
93 #[case::repo(&["repo"], Cmd::Repo(Vec::new()))]
94 #[case::repo_fix(
95 &["repo", "fix", "--clean"],
96 Cmd::Repo(vec![OsString::from("fix"), OsString::from("--clean")])
97 )]
98 #[case::repo_help(&["repo", "--help"], Cmd::Help(Help::Repo))]
99 #[case::repo_fix_help(&["repo", "fix", "--help"], Cmd::Help(Help::RepoFix))]
100 #[case::rsl(&["rsl", "sample.rs"], Cmd::Rsl(vec![OsString::from("sample.rs")]))]
101 #[case::rsl_help(&["rsl", "--help"], Cmd::Help(Help::Rsl))]
102 fn test_parse_known_commands(#[case] args: &[&str], #[case] expected: Cmd) {
103 assert_that!(parse(args), ok(eq(expected)));
104 }
105
106 #[rstest::rstest]
107 #[case::unexpected_argument(&["unexpected"])]
108 #[case::unknown_command(&["unknown", "--value"])]
109 fn test_parse_rejects_invalid_commands(#[case] args: &[&str]) {
110 assert_that!(parse(args), err(anything()));
111 }
112
113 fn parse(args: &[&str]) -> rootcause::Result<Cmd> {
114 Cmd::try_from(Arguments::from_vec(args.iter().map(OsString::from).collect()))
115 }
116}