aperture_cli/engine/
generator.rs1use crate::cache::models::{CachedCommand, CachedParameter, CachedSpec};
16use crate::constants;
17use crate::utils::to_kebab_case;
18use clap::{Arg, ArgAction, Command};
19use std::collections::HashMap;
20use std::fmt::Write;
21
22fn to_static_str(s: String) -> &'static str {
27 Box::leak(s.into_boxed_str())
28}
29
30fn build_help_text_with_examples(cached_command: &CachedCommand) -> String {
32 let mut help_text = cached_command.description.clone().unwrap_or_default();
33
34 if cached_command.examples.is_empty() {
35 return help_text;
36 }
37
38 help_text.push_str("\n\nExamples:");
39 for example in &cached_command.examples {
40 write!(
41 &mut help_text,
42 "\n {}\n {}",
43 example.description, example.command_line
44 )
45 .expect("writing to String buffer cannot fail");
46
47 if let Some(explanation) = example.explanation.as_ref() {
49 write!(&mut help_text, "\n ({explanation})")
50 .expect("writing to String buffer cannot fail");
51 }
52 }
53
54 help_text
55}
56
57#[must_use]
78pub fn generate_command_tree(spec: &CachedSpec) -> Command {
79 generate_command_tree_with_flags(spec, false)
80}
81
82#[must_use]
84pub fn generate_command_tree_with_flags(spec: &CachedSpec, use_positional_args: bool) -> Command {
85 let mut root_command = Command::new(constants::CLI_ROOT_COMMAND)
86 .version(to_static_str(spec.version.clone()))
87 .about(format!("CLI for {} API", spec.name))
88 .arg(
91 Arg::new("jq")
92 .long("jq")
93 .global(true)
94 .hide(true)
95 .help("Apply JQ filter to response data (e.g., '.name', '.[] | select(.active)')")
96 .value_name("FILTER")
97 .action(ArgAction::Set),
98 )
99 .arg(
100 Arg::new("format")
101 .long("format")
102 .global(true)
103 .hide(true)
104 .help("Output format for response data")
105 .value_name("FORMAT")
106 .value_parser(["json", "yaml", "table"])
107 .default_value("json")
108 .action(ArgAction::Set),
109 )
110 .arg(
111 Arg::new("server-var")
112 .long("server-var")
113 .global(true)
114 .hide(true)
115 .help("Set server template variable (e.g., --server-var region=us --server-var env=prod)")
116 .value_name("KEY=VALUE")
117 .action(ArgAction::Append),
118 );
119
120 let mut command_groups: HashMap<String, Vec<&CachedCommand>> = HashMap::new();
122
123 for command in &spec.commands {
124 let group_name = effective_group_name(command);
125 command_groups.entry(group_name).or_default().push(command);
126 }
127
128 for (group_name, commands) in command_groups {
130 let group_name_kebab = to_kebab_case(&group_name);
131 let group_name_static = to_static_str(group_name_kebab);
132 let mut group_command = Command::new(group_name_static)
133 .about(format!("{} operations", capitalize_first(&group_name)));
134
135 for cached_command in commands {
137 let subcommand_name = effective_subcommand_name(cached_command);
138 let subcommand_name_static = to_static_str(subcommand_name);
139
140 let help_text = build_help_text_with_examples(cached_command);
142
143 let mut operation_command = Command::new(subcommand_name_static).about(help_text);
144
145 for param in &cached_command.parameters {
147 let arg = create_arg_from_parameter(param, use_positional_args);
148 operation_command = operation_command.arg(arg);
149 }
150
151 if let Some(request_body) = &cached_command.request_body {
153 operation_command = operation_command.arg(
154 Arg::new("body")
155 .long("body")
156 .help("Request body as JSON")
157 .value_name("JSON")
158 .required(request_body.required)
159 .action(ArgAction::Set),
160 );
161 }
162
163 operation_command = operation_command.arg(
165 Arg::new("header")
166 .long("header")
167 .short('H')
168 .help("Pass custom header(s) to the request. Format: 'Name: Value'. Can be used multiple times.")
169 .value_name("HEADER")
170 .action(ArgAction::Append),
171 );
172
173 operation_command = operation_command.arg(
175 Arg::new("show-examples")
176 .long("show-examples")
177 .help("Show extended usage examples for this command")
178 .action(ArgAction::SetTrue),
179 );
180
181 if !cached_command.aliases.is_empty() {
183 let alias_strs: Vec<&'static str> = cached_command
184 .aliases
185 .iter()
186 .map(|a| to_static_str(to_kebab_case(a)))
187 .collect();
188 operation_command = operation_command.visible_aliases(alias_strs);
189 }
190
191 if cached_command.hidden {
193 operation_command = operation_command.hide(true);
194 }
195
196 group_command = group_command.subcommand(operation_command);
197 }
198
199 root_command = root_command.subcommand(group_command);
200 }
201
202 root_command
203}
204
205fn effective_group_name(command: &CachedCommand) -> String {
207 command.display_group.as_ref().map_or_else(
208 || {
209 if command.name.is_empty() {
210 constants::DEFAULT_GROUP.to_string()
211 } else {
212 command.name.clone()
213 }
214 },
215 Clone::clone,
216 )
217}
218
219fn effective_subcommand_name(command: &CachedCommand) -> String {
221 command.display_name.as_ref().map_or_else(
222 || {
223 if command.operation_id.is_empty() {
224 command.method.to_lowercase()
225 } else {
226 to_kebab_case(&command.operation_id)
227 }
228 },
229 |n| to_kebab_case(n),
230 )
231}
232
233fn create_arg_from_parameter(param: &CachedParameter, use_positional_args: bool) -> Arg {
251 let param_name_static = to_static_str(param.name.clone());
252 let mut arg = Arg::new(param_name_static);
253
254 let is_boolean = param.schema_type.as_ref().is_some_and(|t| t == "boolean");
256
257 match param.location.as_str() {
258 "path" => {
259 match (use_positional_args, is_boolean) {
260 (true, true) => {
261 let long_name = to_static_str(to_kebab_case(¶m.name));
265 arg = arg
266 .long(long_name)
267 .help(format!("Path parameter: {}", param.name))
268 .required(false)
269 .action(ArgAction::SetTrue);
270 }
271 (true, false) => {
272 let value_name = to_static_str(param.name.to_uppercase());
274 arg = arg
275 .help(format!("{} parameter", param.name))
276 .value_name(value_name)
277 .required(param.required)
278 .action(ArgAction::Set);
279 }
280 (false, true) => {
281 let long_name = to_static_str(to_kebab_case(¶m.name));
285 arg = arg
286 .long(long_name)
287 .help(format!("Path parameter: {}", param.name))
288 .required(false)
289 .action(ArgAction::SetTrue);
290 }
291 (false, false) => {
292 let long_name = to_static_str(to_kebab_case(¶m.name));
294 let value_name = to_static_str(param.name.to_uppercase());
295 arg = arg
296 .long(long_name)
297 .help(format!("Path parameter: {}", param.name))
298 .value_name(value_name)
299 .required(param.required)
300 .action(ArgAction::Set);
301 }
302 }
303 }
304 "query" | "header" => {
305 let long_name = to_static_str(to_kebab_case(¶m.name));
307
308 if is_boolean {
309 arg = arg
312 .long(long_name)
313 .help(format!(
314 "{} {} parameter",
315 capitalize_first(¶m.location),
316 param.name
317 ))
318 .required(param.required)
319 .action(ArgAction::SetTrue);
320 return arg;
321 }
322
323 let value_name = to_static_str(param.name.to_uppercase());
324 arg = arg
325 .long(long_name)
326 .help(format!(
327 "{} {} parameter",
328 capitalize_first(¶m.location),
329 param.name
330 ))
331 .value_name(value_name)
332 .required(param.required)
333 .action(ArgAction::Set);
334 }
335 _ => {
336 let long_name = to_static_str(to_kebab_case(¶m.name));
338
339 if is_boolean {
340 arg = arg
343 .long(long_name)
344 .help(format!("{} parameter", param.name))
345 .required(param.required)
346 .action(ArgAction::SetTrue);
347 return arg;
348 }
349
350 let value_name = to_static_str(param.name.to_uppercase());
351 arg = arg
352 .long(long_name)
353 .help(format!("{} parameter", param.name))
354 .value_name(value_name)
355 .required(param.required)
356 .action(ArgAction::Set);
357 }
358 }
359
360 arg
361}
362
363fn capitalize_first(s: &str) -> String {
365 let mut chars = s.chars();
366 chars.next().map_or_else(String::new, |first| {
367 first.to_uppercase().chain(chars).collect()
368 })
369}