1use std::path::PathBuf;
9
10use clap::{Args, Parser, Subcommand};
11
12#[derive(Debug, Parser)]
14#[command(name = "frame", version, about = "Frame application tooling")]
15pub struct Cli {
16 #[command(subcommand)]
18 pub command: Command,
19}
20
21#[derive(Debug, Subcommand)]
23pub enum Command {
24 New {
26 name: String,
28 },
29 Run,
31 Test(TestScope),
34 Check,
37 Doctor,
40 Build,
42 Host {
45 #[arg(long)]
47 config: PathBuf,
48 },
49}
50
51#[derive(Args, Clone, Copy, Debug, Default)]
55pub struct TestScope {
56 #[arg(long)]
58 pub component: bool,
59 #[arg(long)]
61 pub host: bool,
62 #[arg(long)]
64 pub browser: bool,
65}
66
67impl TestScope {
68 #[must_use]
70 pub fn is_everything(self) -> bool {
71 !(self.component || self.host || self.browser)
72 }
73
74 #[must_use]
76 pub fn component_selected(self) -> bool {
77 self.component || self.is_everything()
78 }
79
80 #[must_use]
82 pub fn host_selected(self) -> bool {
83 self.host || self.is_everything()
84 }
85
86 #[must_use]
88 pub fn browser_selected(self) -> bool {
89 self.browser || self.is_everything()
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::{Cli, Command, TestScope};
96 use clap::Parser;
97
98 #[test]
99 fn clap_takes_bare_name_and_rejects_removed_and_unknown_flags() {
100 assert!(Cli::try_parse_from(["frame", "new", "valid_app"]).is_ok());
101 assert!(Cli::try_parse_from(["frame", "new", "valid_app", "--frame-root", "."]).is_err());
102 assert!(Cli::try_parse_from(["frame", "new", "valid_app", "--force"]).is_err());
103 }
104
105 #[test]
106 fn test_scope_defaults_to_everything_and_flags_narrow_to_a_union() -> Result<(), String> {
107 let default = TestScope::default();
108 assert!(default.is_everything());
109 assert!(default.component_selected() && default.host_selected());
110 assert!(default.browser_selected());
111
112 let parsed = Cli::try_parse_from(["frame", "test", "--component", "--browser"])
113 .map_err(|error| error.to_string())?;
114 let Command::Test(scope) = parsed.command else {
115 return Err("frame test --component --browser parsed to the wrong verb".to_owned());
116 };
117 assert!(!scope.is_everything());
118 assert!(scope.component_selected());
119 assert!(scope.browser_selected());
120 assert!(!scope.host_selected());
121 Ok(())
122 }
123
124 #[test]
125 fn every_verb_parses_and_host_requires_its_config() {
126 for verb in ["run", "check", "doctor", "build"] {
127 assert!(Cli::try_parse_from(["frame", verb]).is_ok(), "{verb}");
128 }
129 assert!(Cli::try_parse_from(["frame", "host", "--config", "frame.toml"]).is_ok());
130 assert!(
131 Cli::try_parse_from(["frame", "host"]).is_err(),
132 "frame host without --config must be a parse error"
133 );
134 assert!(
135 Cli::try_parse_from(["frame", "e2e"]).is_err(),
136 "e2e is dead as a command name"
137 );
138 }
139}