fraiseql_cli/
introspection.rs1use clap::Command;
6
7use crate::output::{ArgumentHelp, CliHelp, CommandHelp, CommandSummary, get_exit_codes};
8
9pub fn extract_cli_help(cmd: &Command, version: &str) -> CliHelp {
11 CliHelp {
12 name: cmd.get_name().to_string(),
13 version: version.to_string(),
14 about: cmd.get_about().map_or_else(String::new, ToString::to_string),
15 global_options: extract_global_options(cmd),
16 subcommands: cmd
17 .get_subcommands()
18 .filter(|sub| !sub.is_hide_set())
19 .map(extract_command_help)
20 .collect(),
21 exit_codes: get_exit_codes(),
22 }
23}
24
25pub fn extract_command_help(cmd: &Command) -> CommandHelp {
27 let (arguments, options) = extract_arguments(cmd);
28
29 CommandHelp {
30 name: cmd.get_name().to_string(),
31 about: cmd.get_about().map_or_else(String::new, ToString::to_string),
32 arguments,
33 options,
34 subcommands: cmd
35 .get_subcommands()
36 .filter(|sub| !sub.is_hide_set())
37 .map(extract_command_help)
38 .collect(),
39 examples: extract_examples(cmd),
40 }
41}
42
43pub fn list_commands(cmd: &Command) -> Vec<CommandSummary> {
45 cmd.get_subcommands()
46 .filter(|sub| !sub.is_hide_set())
47 .map(|sub| CommandSummary {
48 name: sub.get_name().to_string(),
49 description: sub.get_about().map_or_else(String::new, ToString::to_string),
50 has_subcommands: sub.get_subcommands().count() > 0,
51 })
52 .collect()
53}
54
55fn extract_global_options(cmd: &Command) -> Vec<ArgumentHelp> {
57 cmd.get_arguments()
58 .filter(|arg| arg.is_global_set())
59 .map(|arg| ArgumentHelp {
60 name: arg.get_id().to_string(),
61 short: arg.get_short().map(|c| format!("-{c}")),
62 long: arg.get_long().map(|s| format!("--{s}")),
63 help: arg.get_help().map_or_else(String::new, ToString::to_string),
64 required: arg.is_required_set(),
65 default_value: arg
66 .get_default_values()
67 .first()
68 .and_then(|v| v.to_str())
69 .map(String::from),
70 takes_value: arg.get_num_args().is_some_and(|n| n.min_values() > 0),
71 possible_values: arg
72 .get_possible_values()
73 .iter()
74 .map(|v| v.get_name().to_string())
75 .collect(),
76 })
77 .collect()
78}
79
80fn extract_arguments(cmd: &Command) -> (Vec<ArgumentHelp>, Vec<ArgumentHelp>) {
82 let mut arguments = Vec::new();
83 let mut options = Vec::new();
84
85 for arg in cmd.get_arguments() {
86 if arg.is_global_set() {
88 continue;
89 }
90
91 let id = arg.get_id().as_str();
93 if id == "help" || id == "version" {
94 continue;
95 }
96
97 let arg_help = ArgumentHelp {
98 name: arg.get_id().to_string(),
99 short: arg.get_short().map(|c| format!("-{c}")),
100 long: arg.get_long().map(|s| format!("--{s}")),
101 help: arg.get_help().map_or_else(String::new, ToString::to_string),
102 required: arg.is_required_set(),
103 default_value: arg
104 .get_default_values()
105 .first()
106 .and_then(|v| v.to_str())
107 .map(String::from),
108 takes_value: arg.get_num_args().is_some_and(|n| n.min_values() > 0),
109 possible_values: arg
110 .get_possible_values()
111 .iter()
112 .map(|v| v.get_name().to_string())
113 .collect(),
114 };
115
116 if arg.get_short().is_none() && arg.get_long().is_none() {
118 arguments.push(arg_help);
119 } else {
120 options.push(arg_help);
121 }
122 }
123
124 (arguments, options)
125}
126
127fn extract_examples(cmd: &Command) -> Vec<String> {
129 if let Some(after_help) = cmd.get_after_help() {
131 let text = after_help.to_string();
132 if let Some(examples_start) = text.find("EXAMPLES:") {
133 let examples_section = &text[examples_start + 9..];
134 return examples_section
135 .lines()
136 .map(str::trim)
137 .filter(|line| !line.is_empty() && line.starts_with("fraiseql"))
138 .map(String::from)
139 .collect();
140 }
141 }
142 Vec::new()
143}
144
145#[allow(clippy::unwrap_used)] #[cfg(test)]
147mod tests {
148 use clap::{Arg, Command as ClapCommand};
149
150 use super::*;
151
152 fn create_test_cli() -> ClapCommand {
153 ClapCommand::new("test-cli")
154 .version("1.0.0")
155 .about("Test CLI for unit tests")
156 .arg(
157 Arg::new("verbose")
158 .short('v')
159 .long("verbose")
160 .help("Enable verbose mode")
161 .global(true)
162 .action(clap::ArgAction::SetTrue),
163 )
164 .subcommand(
165 ClapCommand::new("compile")
166 .about("Compile files")
167 .arg(Arg::new("input").help("Input file").required(true))
168 .arg(
169 Arg::new("output")
170 .short('o')
171 .long("output")
172 .help("Output file")
173 .default_value("out.json"),
174 )
175 .after_help("EXAMPLES:\n fraiseql compile input.json\n fraiseql compile input.json -o output.json"),
176 )
177 .subcommand(
178 ClapCommand::new("hidden")
179 .about("Hidden command")
180 .hide(true),
181 )
182 }
183
184 #[test]
185 fn test_extract_cli_help() {
186 let cmd = create_test_cli();
187 let help = extract_cli_help(&cmd, "1.0.0");
188
189 assert_eq!(help.name, "test-cli");
190 assert_eq!(help.version, "1.0.0");
191 assert_eq!(help.about, "Test CLI for unit tests");
192 assert!(!help.exit_codes.is_empty());
193 }
194
195 #[test]
196 fn test_extract_global_options() {
197 let cmd = create_test_cli();
198 let help = extract_cli_help(&cmd, "1.0.0");
199
200 assert!(!help.global_options.is_empty());
201 let verbose = help.global_options.iter().find(|a| a.name == "verbose");
202 assert!(verbose.is_some());
203 let verbose = verbose.unwrap();
204 assert_eq!(verbose.short, Some("-v".to_string()));
205 assert_eq!(verbose.long, Some("--verbose".to_string()));
206 }
207
208 #[test]
209 fn test_extract_command_help() {
210 let cmd = create_test_cli();
211 let compile = cmd.get_subcommands().find(|c| c.get_name() == "compile").unwrap();
212 let help = extract_command_help(compile);
213
214 assert_eq!(help.name, "compile");
215 assert_eq!(help.about, "Compile files");
216 assert_eq!(help.arguments.len(), 1);
217 assert_eq!(help.arguments[0].name, "input");
218 assert!(help.arguments[0].required);
219 }
220
221 #[test]
222 fn test_extract_options() {
223 let cmd = create_test_cli();
224 let compile = cmd.get_subcommands().find(|c| c.get_name() == "compile").unwrap();
225 let help = extract_command_help(compile);
226
227 let output_opt = help.options.iter().find(|o| o.name == "output");
228 assert!(output_opt.is_some());
229 let output_opt = output_opt.unwrap();
230 assert_eq!(output_opt.default_value, Some("out.json".to_string()));
231 }
232
233 #[test]
234 fn test_extract_examples() {
235 let cmd = create_test_cli();
236 let compile = cmd.get_subcommands().find(|c| c.get_name() == "compile").unwrap();
237 let help = extract_command_help(compile);
238
239 assert_eq!(help.examples.len(), 2);
240 assert!(help.examples[0].contains("fraiseql compile"));
241 }
242
243 #[test]
244 fn test_list_commands() {
245 let cmd = create_test_cli();
246 let commands = list_commands(&cmd);
247
248 assert_eq!(commands.len(), 1);
250 assert_eq!(commands[0].name, "compile");
251 assert!(!commands[0].has_subcommands);
252 }
253
254 #[test]
255 fn test_hidden_commands_excluded() {
256 let cmd = create_test_cli();
257 let help = extract_cli_help(&cmd, "1.0.0");
258
259 let hidden = help.subcommands.iter().find(|s| s.name == "hidden");
261 assert!(hidden.is_none());
262 }
263}