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
16#[must_use]
37pub fn generate_command_tree(spec: &CachedSpec) -> Command {
38 generate_command_tree_with_flags(spec, false)
39}
40
41#[must_use]
43pub fn generate_command_tree_with_flags(spec: &CachedSpec, use_positional_args: bool) -> Command {
44 let mut root_command = Command::new(constants::CLI_ROOT_COMMAND)
45 .version(to_static_str(spec.version.clone()))
46 .about(format!("CLI for {} API", spec.name))
47 .arg(
49 Arg::new("jq")
50 .long("jq")
51 .global(true)
52 .help("Apply JQ filter to response data (e.g., '.name', '.[] | select(.active)')")
53 .value_name("FILTER")
54 .action(ArgAction::Set),
55 )
56 .arg(
57 Arg::new("format")
58 .long("format")
59 .global(true)
60 .help("Output format for response data")
61 .value_name("FORMAT")
62 .value_parser(["json", "yaml", "table"])
63 .default_value("json")
64 .action(ArgAction::Set),
65 )
66 .arg(
67 Arg::new("server-var")
68 .long("server-var")
69 .global(true)
70 .help("Set server template variable (e.g., --server-var region=us --server-var env=prod)")
71 .value_name("KEY=VALUE")
72 .action(ArgAction::Append),
73 );
74
75 let mut command_groups: HashMap<String, Vec<&CachedCommand>> = HashMap::new();
77
78 for command in &spec.commands {
79 let group_name = if command.name.is_empty() {
81 constants::DEFAULT_GROUP.to_string()
82 } else {
83 command.name.clone()
84 };
85
86 command_groups.entry(group_name).or_default().push(command);
87 }
88
89 for (group_name, commands) in command_groups {
91 let group_name_kebab = to_kebab_case(&group_name);
92 let group_name_static = to_static_str(group_name_kebab);
93 let mut group_command = Command::new(group_name_static)
94 .about(format!("{} operations", capitalize_first(&group_name)));
95
96 for cached_command in commands {
98 let subcommand_name = if cached_command.operation_id.is_empty() {
99 cached_command.method.to_lowercase()
101 } else {
102 to_kebab_case(&cached_command.operation_id)
103 };
104
105 let subcommand_name_static = to_static_str(subcommand_name);
106
107 let mut help_text = cached_command.description.clone().unwrap_or_default();
109 if !cached_command.examples.is_empty() {
110 help_text.push_str("\n\nExamples:");
111 for example in &cached_command.examples {
112 write!(
113 &mut help_text,
114 "\n {}\n {}",
115 example.description, example.command_line
116 )
117 .unwrap();
118 if let Some(ref explanation) = example.explanation {
119 write!(&mut help_text, "\n ({explanation})").unwrap();
120 }
121 }
122 }
123
124 let mut operation_command = Command::new(subcommand_name_static).about(help_text);
125
126 for param in &cached_command.parameters {
128 let arg = create_arg_from_parameter(param, use_positional_args);
129 operation_command = operation_command.arg(arg);
130 }
131
132 if let Some(request_body) = &cached_command.request_body {
134 operation_command = operation_command.arg(
135 Arg::new("body")
136 .long("body")
137 .help("Request body as JSON")
138 .value_name("JSON")
139 .required(request_body.required)
140 .action(ArgAction::Set),
141 );
142 }
143
144 operation_command = operation_command.arg(
146 Arg::new("header")
147 .long("header")
148 .short('H')
149 .help("Pass custom header(s) to the request. Format: 'Name: Value'. Can be used multiple times.")
150 .value_name("HEADER")
151 .action(ArgAction::Append),
152 );
153
154 operation_command = operation_command.arg(
156 Arg::new("show-examples")
157 .long("show-examples")
158 .help("Show extended usage examples for this command")
159 .action(ArgAction::SetTrue),
160 );
161
162 group_command = group_command.subcommand(operation_command);
163 }
164
165 root_command = root_command.subcommand(group_command);
166 }
167
168 root_command
169}
170
171fn create_arg_from_parameter(param: &CachedParameter, use_positional_args: bool) -> Arg {
189 let param_name_static = to_static_str(param.name.clone());
190 let mut arg = Arg::new(param_name_static);
191
192 let is_boolean = param.schema_type.as_ref().is_some_and(|t| t == "boolean");
194
195 match param.location.as_str() {
196 "path" => {
197 if use_positional_args {
198 if is_boolean {
200 let long_name = to_static_str(to_kebab_case(¶m.name));
204 arg = arg
205 .long(long_name)
206 .help(format!("Path parameter: {}", param.name))
207 .required(false)
208 .action(ArgAction::SetTrue);
209 } else {
210 let value_name = to_static_str(param.name.to_uppercase());
211 arg = arg
212 .help(format!("{} parameter", param.name))
213 .value_name(value_name)
214 .required(param.required)
215 .action(ArgAction::Set);
216 }
217 } else {
218 let long_name = to_static_str(to_kebab_case(¶m.name));
220
221 if is_boolean {
222 arg = arg
226 .long(long_name)
227 .help(format!("Path parameter: {}", param.name))
228 .required(false)
229 .action(ArgAction::SetTrue);
230 } else {
231 let value_name = to_static_str(param.name.to_uppercase());
232 arg = arg
233 .long(long_name)
234 .help(format!("Path parameter: {}", param.name))
235 .value_name(value_name)
236 .required(param.required)
237 .action(ArgAction::Set);
238 }
239 }
240 }
241 "query" | "header" => {
242 let long_name = to_static_str(to_kebab_case(¶m.name));
244
245 if is_boolean {
246 arg = arg
249 .long(long_name)
250 .help(format!(
251 "{} {} parameter",
252 capitalize_first(¶m.location),
253 param.name
254 ))
255 .required(param.required)
256 .action(ArgAction::SetTrue);
257 } else {
258 let value_name = to_static_str(param.name.to_uppercase());
259 arg = arg
260 .long(long_name)
261 .help(format!(
262 "{} {} parameter",
263 capitalize_first(¶m.location),
264 param.name
265 ))
266 .value_name(value_name)
267 .required(param.required)
268 .action(ArgAction::Set);
269 }
270 }
271 _ => {
272 let long_name = to_static_str(to_kebab_case(¶m.name));
274
275 if is_boolean {
276 arg = arg
279 .long(long_name)
280 .help(format!("{} parameter", param.name))
281 .required(param.required)
282 .action(ArgAction::SetTrue);
283 } else {
284 let value_name = to_static_str(param.name.to_uppercase());
285 arg = arg
286 .long(long_name)
287 .help(format!("{} parameter", param.name))
288 .value_name(value_name)
289 .required(param.required)
290 .action(ArgAction::Set);
291 }
292 }
293 }
294
295 arg
296}
297
298fn capitalize_first(s: &str) -> String {
300 let mut chars = s.chars();
301 chars.next().map_or_else(String::new, |first| {
302 first.to_uppercase().chain(chars).collect()
303 })
304}