use clap::{Arg, ArgAction, Command, CommandFactory};
use serde::Serialize;
use crate::cli::commands::Cli;
#[derive(Debug, Clone, Serialize)]
pub struct CommandReferenceExport {
pub generated_by: String,
pub command_path: Vec<String>,
pub command: CommandNode,
}
#[derive(Debug, Clone, Serialize)]
pub struct CommandNode {
pub name: String,
pub display_name: String,
pub usage: String,
pub about: Option<String>,
pub long_about: Option<String>,
pub after_help: Option<String>,
pub aliases: Vec<String>,
pub visible_aliases: Vec<String>,
pub arguments: Vec<ArgumentNode>,
pub subcommands: Vec<CommandNode>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ArgumentNode {
pub id: String,
pub kind: String,
pub display: String,
pub short: Option<String>,
pub long: Option<String>,
pub help: Option<String>,
pub long_help: Option<String>,
pub required: bool,
pub global: bool,
pub repeatable: bool,
pub positional_index: Option<usize>,
pub default_values: Vec<String>,
pub possible_values: Vec<String>,
}
pub fn export_command_reference(path: &[String]) -> Result<CommandReferenceExport, String> {
let root = sanitize_command_tree(Cli::command());
let command = select_command(root, path)?;
Ok(CommandReferenceExport {
generated_by: "xbp-docs-export".to_string(),
command_path: path.to_vec(),
command: export_command_node(&command, path),
})
}
fn sanitize_command_tree(command: Command) -> Command {
command
.disable_help_flag(true)
.mut_subcommands(sanitize_command_tree)
}
fn select_command(mut command: Command, path: &[String]) -> Result<Command, String> {
for segment in path {
let next = command
.get_subcommands()
.find(|candidate| command_matches(candidate, segment))
.cloned()
.ok_or_else(|| {
let available = command
.get_subcommands()
.map(|candidate| candidate.get_name().to_string())
.collect::<Vec<_>>()
.join(", ");
format!(
"Unknown command path segment `{segment}` under `{}`. Available subcommands: {}",
command.get_name(),
if available.is_empty() { "<none>" } else { &available }
)
})?;
command = next;
}
Ok(command)
}
fn command_matches(command: &Command, segment: &str) -> bool {
command.get_name() == segment
|| command.get_all_aliases().any(|alias| alias == segment)
|| command.get_visible_aliases().any(|alias| alias == segment)
}
fn export_command_node(command: &Command, path: &[String]) -> CommandNode {
let display_name = if path.is_empty() {
"xbp".to_string()
} else {
format!("xbp {}", path.join(" "))
};
let usage = {
let mut usage_command = command.clone();
usage_command
.render_usage()
.to_string()
.replace(
&format!("Usage: {}", command.get_name()),
&format!("Usage: {display_name}"),
)
};
CommandNode {
name: command.get_name().to_string(),
display_name,
usage,
about: command.get_about().map(|value| value.to_string()),
long_about: command.get_long_about().map(|value| value.to_string()),
after_help: command.get_after_help().map(|value| value.to_string()),
aliases: command.get_all_aliases().map(str::to_string).collect(),
visible_aliases: command
.get_visible_aliases()
.map(str::to_string)
.collect(),
arguments: command
.get_arguments()
.filter(|arg| !arg.is_hide_set())
.map(export_argument_node)
.collect(),
subcommands: command
.get_subcommands()
.filter(|subcommand| !subcommand.is_hide_set())
.map(|subcommand| {
let mut next_path = path.to_vec();
next_path.push(subcommand.get_name().to_string());
export_command_node(subcommand, &next_path)
})
.collect(),
}
}
fn export_argument_node(arg: &Arg) -> ArgumentNode {
let default_values = arg
.get_default_values()
.iter()
.map(|value| value.to_string_lossy().to_string())
.collect();
let possible_values = arg
.get_possible_values()
.into_iter()
.map(|value| value.get_name().to_string())
.collect();
let short = arg.get_short().map(|value| format!("-{value}"));
let long = arg.get_long().map(|value| format!("--{value}"));
let positional_index = arg.get_index().map(|value| value as usize);
let value_suffix = if argument_takes_value(arg) {
format!(" {}", argument_value_placeholder(arg))
} else {
String::new()
};
let display = if arg.is_positional() {
argument_value_placeholder(arg)
} else if let Some(long) = long.as_deref() {
format!("{long}{value_suffix}")
} else if let Some(short) = short.as_deref() {
format!("{short}{value_suffix}")
} else {
arg.get_id().as_str().to_string()
};
ArgumentNode {
id: arg.get_id().as_str().to_string(),
kind: if arg.is_positional() {
"positional".to_string()
} else {
"option".to_string()
},
display,
short,
long,
help: arg.get_help().map(|value| value.to_string()),
long_help: arg.get_long_help().map(|value| value.to_string()),
required: arg.is_required_set(),
global: arg.is_global_set(),
repeatable: matches!(arg.get_action(), ArgAction::Append | ArgAction::Count),
positional_index,
default_values,
possible_values,
}
}
fn argument_takes_value(arg: &Arg) -> bool {
matches!(arg.get_action(), ArgAction::Set | ArgAction::Append)
}
fn argument_value_placeholder(arg: &Arg) -> String {
if let Some(names) = arg.get_value_names() {
let rendered = names
.iter()
.map(|name| format!("<{}>", name.to_ascii_uppercase()))
.collect::<Vec<_>>();
if !rendered.is_empty() {
return rendered.join(" ");
}
}
format!("<{}>", arg.get_id().as_str().replace('-', "_").to_ascii_uppercase())
}
#[cfg(test)]
mod tests {
use super::export_command_reference;
#[cfg(feature = "openapi-gen")]
#[test]
fn exports_generate_openapi_reference() {
let export = export_command_reference(&["generate".into(), "openapi".into()])
.expect("generate openapi export");
assert_eq!(export.command.name, "openapi");
assert!(
export
.command
.arguments
.iter()
.any(|argument| argument.id == "service" && argument.display == "--service <SERVICE>")
);
assert!(
export
.command
.arguments
.iter()
.any(|argument| argument.id == "all" && argument.display == "--all")
);
}
}