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]
49pub fn generate_command_tree(spec: &CachedSpec) -> Command {
50 let mut root_command = Command::new("api")
51 .version(to_static_str(spec.version.clone()))
52 .about(format!("CLI for {} API", spec.name));
53
54 let mut command_groups: HashMap<String, Vec<&CachedCommand>> = HashMap::new();
56
57 for command in &spec.commands {
58 let group_name = if command.name.is_empty() {
60 "default".to_string()
61 } else {
62 command.name.clone()
63 };
64
65 command_groups.entry(group_name).or_default().push(command);
66 }
67
68 for (group_name, commands) in command_groups {
70 let group_name_static = to_static_str(group_name.clone());
71 let mut group_command = Command::new(group_name_static)
72 .about(format!("{} operations", capitalize_first(&group_name)));
73
74 for cached_command in commands {
76 let subcommand_name = if cached_command.operation_id.is_empty() {
77 cached_command.method.to_lowercase()
79 } else {
80 to_kebab_case(&cached_command.operation_id)
81 };
82
83 let subcommand_name_static = to_static_str(subcommand_name);
84 let mut operation_command = Command::new(subcommand_name_static)
85 .about(cached_command.description.clone().unwrap_or_default());
86
87 for param in &cached_command.parameters {
89 let arg = create_arg_from_parameter(param);
90 operation_command = operation_command.arg(arg);
91 }
92
93 if let Some(request_body) = &cached_command.request_body {
95 operation_command = operation_command.arg(
96 Arg::new("body")
97 .long("body")
98 .help("Request body as JSON")
99 .value_name("JSON")
100 .required(request_body.required)
101 .action(ArgAction::Set),
102 );
103 }
104
105 operation_command = operation_command.arg(
107 Arg::new("header")
108 .long("header")
109 .short('H')
110 .help("Pass custom header(s) to the request. Format: 'Name: Value'. Can be used multiple times.")
111 .value_name("HEADER")
112 .action(ArgAction::Append),
113 );
114
115 group_command = group_command.subcommand(operation_command);
116 }
117
118 root_command = root_command.subcommand(group_command);
119 }
120
121 root_command
122}
123
124fn create_arg_from_parameter(param: &CachedParameter) -> Arg {
126 let param_name_static = to_static_str(param.name.clone());
127 let mut arg = Arg::new(param_name_static);
128
129 match param.location.as_str() {
130 "path" => {
131 let value_name = to_static_str(param.name.to_uppercase());
133 arg = arg
134 .help(format!("{} parameter", param.name))
135 .value_name(value_name)
136 .required(param.required)
137 .action(ArgAction::Set);
138 }
139 "query" | "header" => {
140 let long_name = to_static_str(param.name.clone());
142 let value_name = to_static_str(param.name.to_uppercase());
143 arg = arg
144 .long(long_name)
145 .help(format!(
146 "{} {} parameter",
147 capitalize_first(¶m.location),
148 param.name
149 ))
150 .value_name(value_name)
151 .required(param.required)
152 .action(ArgAction::Set);
153 }
154 _ => {
155 let long_name = to_static_str(param.name.clone());
157 let value_name = to_static_str(param.name.to_uppercase());
158 arg = arg
159 .long(long_name)
160 .help(format!("{} parameter", param.name))
161 .value_name(value_name)
162 .required(param.required)
163 .action(ArgAction::Set);
164 }
165 }
166
167 arg
168}
169
170fn capitalize_first(s: &str) -> String {
172 let mut chars = s.chars();
173 chars.next().map_or_else(String::new, |first| {
174 first.to_uppercase().chain(chars).collect()
175 })
176}