use crate::cache::models::{CachedCommand, CachedParameter, CachedSpec};
use crate::constants;
use crate::utils::to_kebab_case;
use clap::{Arg, ArgAction, Command};
use std::collections::HashMap;
use std::fmt::Write;
fn to_static_str(s: String) -> &'static str {
Box::leak(s.into_boxed_str())
}
fn build_help_text_with_examples(cached_command: &CachedCommand) -> String {
let mut help_text = cached_command.description.clone().unwrap_or_default();
if cached_command.examples.is_empty() {
return help_text;
}
help_text.push_str("\n\nExamples:");
for example in &cached_command.examples {
write!(
&mut help_text,
"\n {}\n {}",
example.description, example.command_line
)
.expect("writing to String buffer cannot fail");
if let Some(explanation) = example.explanation.as_ref() {
write!(&mut help_text, "\n ({explanation})")
.expect("writing to String buffer cannot fail");
}
}
help_text
}
#[must_use]
pub fn generate_command_tree(spec: &CachedSpec) -> Command {
generate_command_tree_with_flags(spec, false)
}
#[must_use]
pub fn generate_command_tree_with_flags(spec: &CachedSpec, use_positional_args: bool) -> Command {
let mut root_command = Command::new(constants::CLI_ROOT_COMMAND)
.version(to_static_str(spec.version.clone()))
.about(format!("CLI for {} API", spec.name))
.arg(
Arg::new("jq")
.long("jq")
.global(true)
.hide(true)
.help("Apply JQ filter to response data (e.g., '.name', '.[] | select(.active)')")
.value_name("FILTER")
.action(ArgAction::Set),
)
.arg(
Arg::new("format")
.long("format")
.global(true)
.hide(true)
.help("Output format for response data")
.value_name("FORMAT")
.value_parser(["json", "yaml", "table"])
.default_value("json")
.action(ArgAction::Set),
)
.arg(
Arg::new("server-var")
.long("server-var")
.global(true)
.hide(true)
.help("Set server template variable (e.g., --server-var region=us --server-var env=prod)")
.value_name("KEY=VALUE")
.action(ArgAction::Append),
);
let mut command_groups: HashMap<String, Vec<&CachedCommand>> = HashMap::new();
for command in &spec.commands {
let group_name = effective_group_name(command);
command_groups.entry(group_name).or_default().push(command);
}
for (group_name, commands) in command_groups {
let group_name_kebab = to_kebab_case(&group_name);
let group_name_static = to_static_str(group_name_kebab);
let mut group_command = Command::new(group_name_static)
.about(format!("{} operations", capitalize_first(&group_name)));
for cached_command in commands {
let subcommand_name = effective_subcommand_name(cached_command);
let subcommand_name_static = to_static_str(subcommand_name);
let help_text = build_help_text_with_examples(cached_command);
let mut operation_command = Command::new(subcommand_name_static).about(help_text);
for param in &cached_command.parameters {
let arg = create_arg_from_parameter(param, use_positional_args);
operation_command = operation_command.arg(arg);
}
if let Some(request_body) = &cached_command.request_body {
operation_command = add_body_args(operation_command, request_body.required);
}
operation_command = operation_command.arg(
Arg::new("header")
.long("header")
.short('H')
.help("Pass custom header(s) to the request. Format: 'Name: Value'. Can be used multiple times.")
.value_name("HEADER")
.action(ArgAction::Append),
);
operation_command = operation_command.arg(
Arg::new("show-examples")
.long("show-examples")
.help("Show extended usage examples for this command")
.action(ArgAction::SetTrue),
);
if !cached_command.aliases.is_empty() {
let alias_strs: Vec<&'static str> = cached_command
.aliases
.iter()
.map(|a| to_static_str(to_kebab_case(a)))
.collect();
operation_command = operation_command.visible_aliases(alias_strs);
}
if cached_command.hidden {
operation_command = operation_command.hide(true);
}
group_command = group_command.subcommand(operation_command);
}
root_command = root_command.subcommand(group_command);
}
root_command
}
fn add_body_args(cmd: Command, required: bool) -> Command {
let body_arg = Arg::new("body")
.long("body")
.help("Request body as JSON")
.value_name("JSON")
.conflicts_with("body-file")
.action(ArgAction::Set);
let body_arg = if required {
body_arg.required_unless_present("body-file")
} else {
body_arg
};
cmd.arg(body_arg).arg(
Arg::new("body-file")
.long("body-file")
.help("Read request body from a file path, or - for stdin")
.value_name("PATH")
.conflicts_with("body")
.action(ArgAction::Set),
)
}
fn effective_group_name(command: &CachedCommand) -> String {
command.display_group.as_ref().map_or_else(
|| {
if command.name.is_empty() {
constants::DEFAULT_GROUP.to_string()
} else {
command.name.clone()
}
},
Clone::clone,
)
}
fn effective_subcommand_name(command: &CachedCommand) -> String {
command.display_name.as_ref().map_or_else(
|| {
if command.operation_id.is_empty() {
command.method.to_lowercase()
} else {
to_kebab_case(&command.operation_id)
}
},
|n| to_kebab_case(n),
)
}
fn create_arg_from_parameter(param: &CachedParameter, use_positional_args: bool) -> Arg {
let param_name_static = to_static_str(param.name.clone());
let mut arg = Arg::new(param_name_static);
let is_boolean = param.schema_type.as_ref().is_some_and(|t| t == "boolean");
match param.location.as_str() {
"path" => {
match (use_positional_args, is_boolean) {
(true, true) => {
let long_name = to_static_str(to_kebab_case(¶m.name));
arg = arg
.long(long_name)
.help(format!("Path parameter: {}", param.name))
.required(false)
.action(ArgAction::SetTrue);
}
(true, false) => {
let value_name = to_static_str(param.name.to_uppercase());
arg = arg
.help(format!("{} parameter", param.name))
.value_name(value_name)
.required(param.required)
.action(ArgAction::Set);
}
(false, true) => {
let long_name = to_static_str(to_kebab_case(¶m.name));
arg = arg
.long(long_name)
.help(format!("Path parameter: {}", param.name))
.required(false)
.action(ArgAction::SetTrue);
}
(false, false) => {
let long_name = to_static_str(to_kebab_case(¶m.name));
let value_name = to_static_str(param.name.to_uppercase());
arg = arg
.long(long_name)
.help(format!("Path parameter: {}", param.name))
.value_name(value_name)
.required(param.required)
.action(ArgAction::Set);
}
}
}
"query" | "header" => {
let long_name = to_static_str(to_kebab_case(¶m.name));
if is_boolean {
arg = arg
.long(long_name)
.help(format!(
"{} {} parameter",
capitalize_first(¶m.location),
param.name
))
.required(param.required)
.action(ArgAction::SetTrue);
return arg;
}
let value_name = to_static_str(param.name.to_uppercase());
arg = arg
.long(long_name)
.help(format!(
"{} {} parameter",
capitalize_first(¶m.location),
param.name
))
.value_name(value_name)
.required(param.required)
.action(ArgAction::Set);
}
_ => {
let long_name = to_static_str(to_kebab_case(¶m.name));
if is_boolean {
arg = arg
.long(long_name)
.help(format!("{} parameter", param.name))
.required(param.required)
.action(ArgAction::SetTrue);
return arg;
}
let value_name = to_static_str(param.name.to_uppercase());
arg = arg
.long(long_name)
.help(format!("{} parameter", param.name))
.value_name(value_name)
.required(param.required)
.action(ArgAction::Set);
}
}
arg
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
chars.next().map_or_else(String::new, |first| {
first.to_uppercase().chain(chars).collect()
})
}