aperture_cli/engine/
generator.rs1use crate::cache::models::{CachedCommand, CachedParameter, CachedSpec};
2use crate::constants;
3use crate::utils::to_kebab_case;
4use clap::{Arg, ArgAction, Command};
5use std::collections::HashMap;
6use std::fmt::Write;
7
8fn to_static_str(s: String) -> &'static str {
13 Box::leak(s.into_boxed_str())
14}
15
16fn build_help_text_with_examples(cached_command: &CachedCommand) -> String {
18 let mut help_text = cached_command.description.clone().unwrap_or_default();
19
20 if cached_command.examples.is_empty() {
21 return help_text;
22 }
23
24 help_text.push_str("\n\nExamples:");
25 for example in &cached_command.examples {
26 write!(
27 &mut help_text,
28 "\n {}\n {}",
29 example.description, example.command_line
30 )
31 .expect("writing to String buffer cannot fail");
32
33 if let Some(explanation) = example.explanation.as_ref() {
35 write!(&mut help_text, "\n ({explanation})")
36 .expect("writing to String buffer cannot fail");
37 }
38 }
39
40 help_text
41}
42
43#[must_use]
64pub fn generate_command_tree(spec: &CachedSpec) -> Command {
65 generate_command_tree_with_flags(spec, false)
66}
67
68#[must_use]
70pub fn generate_command_tree_with_flags(spec: &CachedSpec, use_positional_args: bool) -> Command {
71 let mut root_command = Command::new(constants::CLI_ROOT_COMMAND)
72 .version(to_static_str(spec.version.clone()))
73 .about(format!("CLI for {} API", spec.name))
74 .arg(
77 Arg::new("jq")
78 .long("jq")
79 .global(true)
80 .hide(true)
81 .help("Apply JQ filter to response data (e.g., '.name', '.[] | select(.active)')")
82 .value_name("FILTER")
83 .action(ArgAction::Set),
84 )
85 .arg(
86 Arg::new("format")
87 .long("format")
88 .global(true)
89 .hide(true)
90 .help("Output format for response data")
91 .value_name("FORMAT")
92 .value_parser(["json", "yaml", "table"])
93 .default_value("json")
94 .action(ArgAction::Set),
95 )
96 .arg(
97 Arg::new("server-var")
98 .long("server-var")
99 .global(true)
100 .hide(true)
101 .help("Set server template variable (e.g., --server-var region=us --server-var env=prod)")
102 .value_name("KEY=VALUE")
103 .action(ArgAction::Append),
104 );
105
106 let mut command_groups: HashMap<String, Vec<&CachedCommand>> = HashMap::new();
108
109 for command in &spec.commands {
110 let group_name = if command.name.is_empty() {
112 constants::DEFAULT_GROUP.to_string()
113 } else {
114 command.name.clone()
115 };
116
117 command_groups.entry(group_name).or_default().push(command);
118 }
119
120 for (group_name, commands) in command_groups {
122 let group_name_kebab = to_kebab_case(&group_name);
123 let group_name_static = to_static_str(group_name_kebab);
124 let mut group_command = Command::new(group_name_static)
125 .about(format!("{} operations", capitalize_first(&group_name)));
126
127 for cached_command in commands {
129 let subcommand_name = if cached_command.operation_id.is_empty() {
130 cached_command.method.to_lowercase()
132 } else {
133 to_kebab_case(&cached_command.operation_id)
134 };
135
136 let subcommand_name_static = to_static_str(subcommand_name);
137
138 let help_text = build_help_text_with_examples(cached_command);
140
141 let mut operation_command = Command::new(subcommand_name_static).about(help_text);
142
143 for param in &cached_command.parameters {
145 let arg = create_arg_from_parameter(param, use_positional_args);
146 operation_command = operation_command.arg(arg);
147 }
148
149 if let Some(request_body) = &cached_command.request_body {
151 operation_command = operation_command.arg(
152 Arg::new("body")
153 .long("body")
154 .help("Request body as JSON")
155 .value_name("JSON")
156 .required(request_body.required)
157 .action(ArgAction::Set),
158 );
159 }
160
161 operation_command = operation_command.arg(
163 Arg::new("header")
164 .long("header")
165 .short('H')
166 .help("Pass custom header(s) to the request. Format: 'Name: Value'. Can be used multiple times.")
167 .value_name("HEADER")
168 .action(ArgAction::Append),
169 );
170
171 operation_command = operation_command.arg(
173 Arg::new("show-examples")
174 .long("show-examples")
175 .help("Show extended usage examples for this command")
176 .action(ArgAction::SetTrue),
177 );
178
179 group_command = group_command.subcommand(operation_command);
180 }
181
182 root_command = root_command.subcommand(group_command);
183 }
184
185 root_command
186}
187
188fn create_arg_from_parameter(param: &CachedParameter, use_positional_args: bool) -> Arg {
206 let param_name_static = to_static_str(param.name.clone());
207 let mut arg = Arg::new(param_name_static);
208
209 let is_boolean = param.schema_type.as_ref().is_some_and(|t| t == "boolean");
211
212 match param.location.as_str() {
213 "path" => {
214 match (use_positional_args, is_boolean) {
215 (true, true) => {
216 let long_name = to_static_str(to_kebab_case(¶m.name));
220 arg = arg
221 .long(long_name)
222 .help(format!("Path parameter: {}", param.name))
223 .required(false)
224 .action(ArgAction::SetTrue);
225 }
226 (true, false) => {
227 let value_name = to_static_str(param.name.to_uppercase());
229 arg = arg
230 .help(format!("{} parameter", param.name))
231 .value_name(value_name)
232 .required(param.required)
233 .action(ArgAction::Set);
234 }
235 (false, true) => {
236 let long_name = to_static_str(to_kebab_case(¶m.name));
240 arg = arg
241 .long(long_name)
242 .help(format!("Path parameter: {}", param.name))
243 .required(false)
244 .action(ArgAction::SetTrue);
245 }
246 (false, false) => {
247 let long_name = to_static_str(to_kebab_case(¶m.name));
249 let value_name = to_static_str(param.name.to_uppercase());
250 arg = arg
251 .long(long_name)
252 .help(format!("Path parameter: {}", param.name))
253 .value_name(value_name)
254 .required(param.required)
255 .action(ArgAction::Set);
256 }
257 }
258 }
259 "query" | "header" => {
260 let long_name = to_static_str(to_kebab_case(¶m.name));
262
263 if is_boolean {
264 arg = arg
267 .long(long_name)
268 .help(format!(
269 "{} {} parameter",
270 capitalize_first(¶m.location),
271 param.name
272 ))
273 .required(param.required)
274 .action(ArgAction::SetTrue);
275 return arg;
276 }
277
278 let value_name = to_static_str(param.name.to_uppercase());
279 arg = arg
280 .long(long_name)
281 .help(format!(
282 "{} {} parameter",
283 capitalize_first(¶m.location),
284 param.name
285 ))
286 .value_name(value_name)
287 .required(param.required)
288 .action(ArgAction::Set);
289 }
290 _ => {
291 let long_name = to_static_str(to_kebab_case(¶m.name));
293
294 if is_boolean {
295 arg = arg
298 .long(long_name)
299 .help(format!("{} parameter", param.name))
300 .required(param.required)
301 .action(ArgAction::SetTrue);
302 return arg;
303 }
304
305 let value_name = to_static_str(param.name.to_uppercase());
306 arg = arg
307 .long(long_name)
308 .help(format!("{} parameter", param.name))
309 .value_name(value_name)
310 .required(param.required)
311 .action(ArgAction::Set);
312 }
313 }
314
315 arg
316}
317
318fn capitalize_first(s: &str) -> String {
320 let mut chars = s.chars();
321 chars.next().map_or_else(String::new, |first| {
322 first.to_uppercase().chain(chars).collect()
323 })
324}