aperture_cli/engine/
generator.rs1use crate::cache::models::{CachedCommand, CachedParameter, CachedSpec};
2use crate::utils::to_kebab_case;
3use clap::{Arg, ArgAction, Command};
4use std::collections::HashMap;
5
6fn to_static_str(s: String) -> &'static str {
11 Box::leak(s.into_boxed_str())
12}
13
14#[must_use]
35pub fn generate_command_tree(spec: &CachedSpec) -> Command {
36 generate_command_tree_with_flags(spec, false)
37}
38
39#[must_use]
41pub fn generate_command_tree_with_flags(spec: &CachedSpec, use_positional_args: bool) -> Command {
42 let mut root_command = Command::new("api")
43 .version(to_static_str(spec.version.clone()))
44 .about(format!("CLI for {} API", spec.name))
45 .arg(
47 Arg::new("jq")
48 .long("jq")
49 .global(true)
50 .help("Apply JQ filter to response data (e.g., '.name', '.[] | select(.active)')")
51 .value_name("FILTER")
52 .action(ArgAction::Set),
53 )
54 .arg(
55 Arg::new("format")
56 .long("format")
57 .global(true)
58 .help("Output format for response data")
59 .value_name("FORMAT")
60 .value_parser(["json", "yaml", "table"])
61 .default_value("json")
62 .action(ArgAction::Set),
63 )
64 .arg(
65 Arg::new("server-var")
66 .long("server-var")
67 .global(true)
68 .help("Set server template variable (e.g., --server-var region=us --server-var env=prod)")
69 .value_name("KEY=VALUE")
70 .action(ArgAction::Append),
71 );
72
73 let mut command_groups: HashMap<String, Vec<&CachedCommand>> = HashMap::new();
75
76 for command in &spec.commands {
77 let group_name = if command.name.is_empty() {
79 "default".to_string()
80 } else {
81 command.name.clone()
82 };
83
84 command_groups.entry(group_name).or_default().push(command);
85 }
86
87 for (group_name, commands) in command_groups {
89 let group_name_static = to_static_str(group_name.to_lowercase());
90 let mut group_command = Command::new(group_name_static)
91 .about(format!("{} operations", capitalize_first(&group_name)));
92
93 for cached_command in commands {
95 let subcommand_name = if cached_command.operation_id.is_empty() {
96 cached_command.method.to_lowercase()
98 } else {
99 to_kebab_case(&cached_command.operation_id)
100 };
101
102 let subcommand_name_static = to_static_str(subcommand_name);
103 let mut operation_command = Command::new(subcommand_name_static)
104 .about(cached_command.description.clone().unwrap_or_default());
105
106 for param in &cached_command.parameters {
108 let arg = create_arg_from_parameter(param, use_positional_args);
109 operation_command = operation_command.arg(arg);
110 }
111
112 if let Some(request_body) = &cached_command.request_body {
114 operation_command = operation_command.arg(
115 Arg::new("body")
116 .long("body")
117 .help("Request body as JSON")
118 .value_name("JSON")
119 .required(request_body.required)
120 .action(ArgAction::Set),
121 );
122 }
123
124 operation_command = operation_command.arg(
126 Arg::new("header")
127 .long("header")
128 .short('H')
129 .help("Pass custom header(s) to the request. Format: 'Name: Value'. Can be used multiple times.")
130 .value_name("HEADER")
131 .action(ArgAction::Append),
132 );
133
134 group_command = group_command.subcommand(operation_command);
135 }
136
137 root_command = root_command.subcommand(group_command);
138 }
139
140 root_command
141}
142
143fn create_arg_from_parameter(param: &CachedParameter, use_positional_args: bool) -> Arg {
145 let param_name_static = to_static_str(param.name.clone());
146 let mut arg = Arg::new(param_name_static);
147
148 match param.location.as_str() {
149 "path" => {
150 if use_positional_args {
151 let value_name = to_static_str(param.name.to_uppercase());
153 arg = arg
154 .help(format!("{} parameter", param.name))
155 .value_name(value_name)
156 .required(param.required)
157 .action(ArgAction::Set);
158 } else {
159 let long_name = to_static_str(param.name.clone());
161 let value_name = to_static_str(param.name.to_uppercase());
162 arg = arg
163 .long(long_name)
164 .help(format!("Path parameter: {}", param.name))
165 .value_name(value_name)
166 .required(param.required)
167 .action(ArgAction::Set);
168 }
169 }
170 "query" | "header" => {
171 let long_name = to_static_str(param.name.clone());
173 let value_name = to_static_str(param.name.to_uppercase());
174 arg = arg
175 .long(long_name)
176 .help(format!(
177 "{} {} parameter",
178 capitalize_first(¶m.location),
179 param.name
180 ))
181 .value_name(value_name)
182 .required(param.required)
183 .action(ArgAction::Set);
184 }
185 _ => {
186 let long_name = to_static_str(param.name.clone());
188 let value_name = to_static_str(param.name.to_uppercase());
189 arg = arg
190 .long(long_name)
191 .help(format!("{} parameter", param.name))
192 .value_name(value_name)
193 .required(param.required)
194 .action(ArgAction::Set);
195 }
196 }
197
198 arg
199}
200
201fn capitalize_first(s: &str) -> String {
203 let mut chars = s.chars();
204 chars.next().map_or_else(String::new, |first| {
205 first.to_uppercase().chain(chars).collect()
206 })
207}