1use clap::Parser;
10
11use face_core::OutputFormat;
12
13#[derive(Debug, Parser)]
20#[command(
21 name = "face",
22 version,
23 about = "Fold And Cluster Entries — group, page, and summarize structured command output."
24)]
25#[non_exhaustive]
26pub struct Cli {
27 #[arg(long = "format-in", help_heading = "INPUT")]
30 pub format_in: Option<String>,
31
32 #[arg(long, help_heading = "INPUT")]
34 pub items: Option<String>,
35
36 #[arg(long, value_delimiter = ',', help_heading = "INPUT")]
38 pub columns: Option<Vec<String>>,
39
40 #[arg(long, help_heading = "SCORE")]
43 pub score: Option<String>,
44
45 #[arg(long, help_heading = "SCORE")]
47 pub scale: Option<f64>,
48
49 #[arg(long, help_heading = "SCORE")]
51 pub invert: bool,
52
53 #[arg(long, help_heading = "SCORE")]
56 pub preset: Option<String>,
57
58 #[arg(long, value_delimiter = ',', help_heading = "STRATEGY")]
61 pub by: Option<Vec<String>>,
62
63 #[arg(long, help_heading = "STRATEGY")]
66 pub strategy: Option<String>,
67
68 #[arg(long, help_heading = "STRATEGY")]
70 pub bands: Option<u8>,
71
72 #[arg(long, help_heading = "STRATEGY")]
74 pub top: Option<u32>,
75
76 #[arg(long, help_heading = "STRATEGY")]
78 pub within: Vec<String>,
79
80 #[arg(long, help_heading = "PAGE")]
85 pub cluster: Vec<String>,
86
87 #[arg(long, help_heading = "PAGE")]
89 pub page: Option<u32>,
90
91 #[arg(long, help_heading = "PAGE")]
93 pub per_page: Option<u32>,
94
95 #[arg(
99 long,
100 value_parser = parse_format,
101 help_heading = "OUTPUT",
102 )]
103 pub format: Option<OutputFormat>,
104
105 #[arg(long = "json", short = 'j', help_heading = "OUTPUT")]
107 pub json: bool,
108
109 #[arg(
111 long,
112 value_parser = clap::builder::PossibleValuesParser::new(["auto", "always", "never"]),
113 help_heading = "OUTPUT",
114 )]
115 pub color: Option<String>,
116
117 #[arg(long, help_heading = "MODES")]
120 pub explain: bool,
121
122 #[arg(long, help_heading = "MODES")]
124 pub schema: bool,
125
126 #[arg(long, help_heading = "MODES")]
128 pub interactive: bool,
129
130 #[arg(long, help_heading = "MODES")]
132 pub verbose: bool,
133}
134
135fn parse_format(s: &str) -> Result<OutputFormat, String> {
138 serde_json::from_value(serde_json::Value::String(s.to_string()))
139 .map_err(|e| format!("invalid format `{s}`: {e}"))
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use clap::Parser;
146
147 #[test]
148 fn parses_minimal_invocation() {
149 let cli = Cli::try_parse_from(["face"]).unwrap();
150 assert_eq!(cli.format, None);
151 assert_eq!(cli.color, None);
152 assert!(!cli.verbose);
153 assert!(!cli.interactive);
154 assert!(!cli.explain);
155 assert!(!cli.json);
156 assert_eq!(cli.page, None);
157 assert_eq!(cli.per_page, None);
158 }
159
160 #[test]
161 fn parses_format_kebab_case() {
162 let cli = Cli::try_parse_from(["face", "--format=json-flat"]).unwrap();
163 assert_eq!(cli.format, Some(OutputFormat::JsonFlat));
164 }
165
166 #[test]
167 fn json_short_flag_sets_alias() {
168 let cli = Cli::try_parse_from(["face", "-j"]).unwrap();
169 assert!(cli.json);
170 assert_eq!(cli.format, None);
172 }
173
174 #[test]
175 fn json_long_flag_sets_alias() {
176 let cli = Cli::try_parse_from(["face", "--json"]).unwrap();
177 assert!(cli.json);
178 assert_eq!(cli.format, None);
179 }
180
181 #[test]
182 fn by_splits_on_comma() {
183 let cli = Cli::try_parse_from(["face", "--by", "file,module"]).unwrap();
184 assert_eq!(cli.by, Some(vec!["file".to_string(), "module".to_string()]));
185 }
186
187 #[test]
188 fn within_is_repeatable() {
189 let cli = Cli::try_parse_from(["face", "--within", "score", "--within", "tag"]).unwrap();
190 assert_eq!(cli.within, vec!["score".to_string(), "tag".to_string()]);
191 }
192
193 #[test]
194 fn cluster_is_repeatable() {
195 let cli = Cli::try_parse_from([
196 "face",
197 "--cluster",
198 "file=src/cli.rs",
199 "--cluster",
200 "score=excellent",
201 ])
202 .unwrap();
203 assert_eq!(cli.cluster.len(), 2);
204 }
205
206 #[test]
207 fn interactive_flag_parses() {
208 let cli = Cli::try_parse_from(["face", "--interactive"]).unwrap();
209 assert!(cli.interactive);
210 }
211
212 #[test]
213 fn verbose_flag_parses() {
214 let cli = Cli::try_parse_from(["face", "--verbose"]).unwrap();
215 assert!(cli.verbose);
216 }
217
218 #[test]
219 fn quiet_flag_is_not_supported() {
220 let err = Cli::try_parse_from(["face", "--quiet"]).unwrap_err();
221 assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
222 }
223}