aperture_cli/engine/
generator.rs1use crate::cache::models::{CachedCommand, CachedParameter, CachedSpec};
2use clap::{Arg, ArgAction, Command};
3use std::collections::HashMap;
4
5fn to_kebab_case(s: &str) -> String {
7 let mut result = String::new();
8 let mut prev_lowercase = false;
9
10 for (i, ch) in s.chars().enumerate() {
11 if ch.is_uppercase() && i > 0 && prev_lowercase {
12 result.push('-');
13 }
14 result.push(ch.to_ascii_lowercase());
15 prev_lowercase = ch.is_lowercase();
16 }
17
18 result
19}
20
21fn to_static_str(s: String) -> &'static str {
26 Box::leak(s.into_boxed_str())
27}
28
29#[must_use]
50pub fn generate_command_tree(spec: &CachedSpec) -> Command {
51 generate_command_tree_with_flags(spec, false)
52}
53
54#[must_use]
56pub fn generate_command_tree_with_flags(spec: &CachedSpec, use_positional_args: bool) -> Command {
57 let mut root_command = Command::new("api")
58 .version(to_static_str(spec.version.clone()))
59 .about(format!("CLI for {} API", spec.name))
60 .arg(
62 Arg::new("jq")
63 .long("jq")
64 .global(true)
65 .help("Apply JQ filter to response data (e.g., '.name', '.[] | select(.active)')")
66 .value_name("FILTER")
67 .action(ArgAction::Set),
68 )
69 .arg(
70 Arg::new("format")
71 .long("format")
72 .global(true)
73 .help("Output format for response data")
74 .value_name("FORMAT")
75 .value_parser(["json", "yaml", "table"])
76 .default_value("json")
77 .action(ArgAction::Set),
78 );
79
80 let mut command_groups: HashMap<String, Vec<&CachedCommand>> = HashMap::new();
82
83 for command in &spec.commands {
84 let group_name = if command.name.is_empty() {
86 "default".to_string()
87 } else {
88 command.name.clone()
89 };
90
91 command_groups.entry(group_name).or_default().push(command);
92 }
93
94 for (group_name, commands) in command_groups {
96 let group_name_static = to_static_str(group_name.clone());
97 let mut group_command = Command::new(group_name_static)
98 .about(format!("{} operations", capitalize_first(&group_name)));
99
100 for cached_command in commands {
102 let subcommand_name = if cached_command.operation_id.is_empty() {
103 cached_command.method.to_lowercase()
105 } else {
106 to_kebab_case(&cached_command.operation_id)
107 };
108
109 let subcommand_name_static = to_static_str(subcommand_name);
110 let mut operation_command = Command::new(subcommand_name_static)
111 .about(cached_command.description.clone().unwrap_or_default());
112
113 for param in &cached_command.parameters {
115 let arg = create_arg_from_parameter(param, use_positional_args);
116 operation_command = operation_command.arg(arg);
117 }
118
119 if let Some(request_body) = &cached_command.request_body {
121 operation_command = operation_command.arg(
122 Arg::new("body")
123 .long("body")
124 .help("Request body as JSON")
125 .value_name("JSON")
126 .required(request_body.required)
127 .action(ArgAction::Set),
128 );
129 }
130
131 operation_command = operation_command.arg(
133 Arg::new("header")
134 .long("header")
135 .short('H')
136 .help("Pass custom header(s) to the request. Format: 'Name: Value'. Can be used multiple times.")
137 .value_name("HEADER")
138 .action(ArgAction::Append),
139 );
140
141 group_command = group_command.subcommand(operation_command);
142 }
143
144 root_command = root_command.subcommand(group_command);
145 }
146
147 root_command
148}
149
150fn create_arg_from_parameter(param: &CachedParameter, use_positional_args: bool) -> Arg {
152 let param_name_static = to_static_str(param.name.clone());
153 let mut arg = Arg::new(param_name_static);
154
155 match param.location.as_str() {
156 "path" => {
157 if use_positional_args {
158 let value_name = to_static_str(param.name.to_uppercase());
160 arg = arg
161 .help(format!("{} parameter", param.name))
162 .value_name(value_name)
163 .required(param.required)
164 .action(ArgAction::Set);
165 } else {
166 let long_name = to_static_str(param.name.clone());
168 let value_name = to_static_str(param.name.to_uppercase());
169 arg = arg
170 .long(long_name)
171 .help(format!("Path parameter: {}", param.name))
172 .value_name(value_name)
173 .required(param.required)
174 .action(ArgAction::Set);
175 }
176 }
177 "query" | "header" => {
178 let long_name = to_static_str(param.name.clone());
180 let value_name = to_static_str(param.name.to_uppercase());
181 arg = arg
182 .long(long_name)
183 .help(format!(
184 "{} {} parameter",
185 capitalize_first(¶m.location),
186 param.name
187 ))
188 .value_name(value_name)
189 .required(param.required)
190 .action(ArgAction::Set);
191 }
192 _ => {
193 let long_name = to_static_str(param.name.clone());
195 let value_name = to_static_str(param.name.to_uppercase());
196 arg = arg
197 .long(long_name)
198 .help(format!("{} parameter", param.name))
199 .value_name(value_name)
200 .required(param.required)
201 .action(ArgAction::Set);
202 }
203 }
204
205 arg
206}
207
208fn capitalize_first(s: &str) -> String {
210 let mut chars = s.chars();
211 chars.next().map_or_else(String::new, |first| {
212 first.to_uppercase().chain(chars).collect()
213 })
214}