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