1use clap::{Arg, ArgAction, Command, CommandFactory};
2use serde::Serialize;
3
4use crate::cli::commands::Cli;
5
6#[derive(Debug, Clone, Serialize)]
7pub struct CommandReferenceExport {
8 pub generated_by: String,
9 pub command_path: Vec<String>,
10 pub command: CommandNode,
11}
12
13#[derive(Debug, Clone, Serialize)]
14pub struct CommandNode {
15 pub name: String,
16 pub display_name: String,
17 pub usage: String,
18 pub about: Option<String>,
19 pub long_about: Option<String>,
20 pub after_help: Option<String>,
21 pub aliases: Vec<String>,
22 pub visible_aliases: Vec<String>,
23 pub arguments: Vec<ArgumentNode>,
24 pub subcommands: Vec<CommandNode>,
25}
26
27#[derive(Debug, Clone, Serialize)]
28pub struct ArgumentNode {
29 pub id: String,
30 pub kind: String,
31 pub display: String,
32 pub short: Option<String>,
33 pub long: Option<String>,
34 pub help: Option<String>,
35 pub long_help: Option<String>,
36 pub required: bool,
37 pub global: bool,
38 pub repeatable: bool,
39 pub positional_index: Option<usize>,
40 pub default_values: Vec<String>,
41 pub possible_values: Vec<String>,
42}
43
44pub fn export_command_reference(path: &[String]) -> Result<CommandReferenceExport, String> {
45 let root = sanitize_command_tree(Cli::command());
46 let command = select_command(root, path)?;
47
48 Ok(CommandReferenceExport {
49 generated_by: "xbp-docs-export".to_string(),
50 command_path: path.to_vec(),
51 command: export_command_node(&command, path),
52 })
53}
54
55fn sanitize_command_tree(command: Command) -> Command {
56 command
57 .disable_help_flag(true)
58 .mut_subcommands(sanitize_command_tree)
59}
60
61fn select_command(mut command: Command, path: &[String]) -> Result<Command, String> {
62 for segment in path {
63 let next = command
64 .get_subcommands()
65 .find(|candidate| command_matches(candidate, segment))
66 .cloned()
67 .ok_or_else(|| {
68 let available = command
69 .get_subcommands()
70 .map(|candidate| candidate.get_name().to_string())
71 .collect::<Vec<_>>()
72 .join(", ");
73 format!(
74 "Unknown command path segment `{segment}` under `{}`. Available subcommands: {}",
75 command.get_name(),
76 if available.is_empty() { "<none>" } else { &available }
77 )
78 })?;
79 command = next;
80 }
81
82 Ok(command)
83}
84
85fn command_matches(command: &Command, segment: &str) -> bool {
86 command.get_name() == segment
87 || command.get_all_aliases().any(|alias| alias == segment)
88 || command.get_visible_aliases().any(|alias| alias == segment)
89}
90
91fn export_command_node(command: &Command, path: &[String]) -> CommandNode {
92 let display_name = if path.is_empty() {
93 "xbp".to_string()
94 } else {
95 format!("xbp {}", path.join(" "))
96 };
97 let usage = {
98 let mut usage_command = command.clone();
99 usage_command
100 .render_usage()
101 .to_string()
102 .replace(
103 &format!("Usage: {}", command.get_name()),
104 &format!("Usage: {display_name}"),
105 )
106 };
107
108 CommandNode {
109 name: command.get_name().to_string(),
110 display_name,
111 usage,
112 about: command.get_about().map(|value| value.to_string()),
113 long_about: command.get_long_about().map(|value| value.to_string()),
114 after_help: command.get_after_help().map(|value| value.to_string()),
115 aliases: command.get_all_aliases().map(str::to_string).collect(),
116 visible_aliases: command
117 .get_visible_aliases()
118 .map(str::to_string)
119 .collect(),
120 arguments: command
121 .get_arguments()
122 .filter(|arg| !arg.is_hide_set())
123 .map(export_argument_node)
124 .collect(),
125 subcommands: command
126 .get_subcommands()
127 .filter(|subcommand| !subcommand.is_hide_set())
128 .map(|subcommand| {
129 let mut next_path = path.to_vec();
130 next_path.push(subcommand.get_name().to_string());
131 export_command_node(subcommand, &next_path)
132 })
133 .collect(),
134 }
135}
136
137fn export_argument_node(arg: &Arg) -> ArgumentNode {
138 let default_values = arg
139 .get_default_values()
140 .iter()
141 .map(|value| value.to_string_lossy().to_string())
142 .collect();
143 let possible_values = arg
144 .get_possible_values()
145 .into_iter()
146 .map(|value| value.get_name().to_string())
147 .collect();
148 let short = arg.get_short().map(|value| format!("-{value}"));
149 let long = arg.get_long().map(|value| format!("--{value}"));
150 let positional_index = arg.get_index().map(|value| value as usize);
151 let value_suffix = if argument_takes_value(arg) {
152 format!(" {}", argument_value_placeholder(arg))
153 } else {
154 String::new()
155 };
156
157 let display = if arg.is_positional() {
158 argument_value_placeholder(arg)
159 } else if let Some(long) = long.as_deref() {
160 format!("{long}{value_suffix}")
161 } else if let Some(short) = short.as_deref() {
162 format!("{short}{value_suffix}")
163 } else {
164 arg.get_id().as_str().to_string()
165 };
166
167 ArgumentNode {
168 id: arg.get_id().as_str().to_string(),
169 kind: if arg.is_positional() {
170 "positional".to_string()
171 } else {
172 "option".to_string()
173 },
174 display,
175 short,
176 long,
177 help: arg.get_help().map(|value| value.to_string()),
178 long_help: arg.get_long_help().map(|value| value.to_string()),
179 required: arg.is_required_set(),
180 global: arg.is_global_set(),
181 repeatable: matches!(arg.get_action(), ArgAction::Append | ArgAction::Count),
182 positional_index,
183 default_values,
184 possible_values,
185 }
186}
187
188fn argument_takes_value(arg: &Arg) -> bool {
189 matches!(arg.get_action(), ArgAction::Set | ArgAction::Append)
190}
191
192fn argument_value_placeholder(arg: &Arg) -> String {
193 if let Some(names) = arg.get_value_names() {
194 let rendered = names
195 .iter()
196 .map(|name| format!("<{}>", name.to_ascii_uppercase()))
197 .collect::<Vec<_>>();
198 if !rendered.is_empty() {
199 return rendered.join(" ");
200 }
201 }
202
203 format!("<{}>", arg.get_id().as_str().replace('-', "_").to_ascii_uppercase())
204}
205
206#[cfg(test)]
207mod tests {
208 use super::export_command_reference;
209
210 #[cfg(feature = "openapi-gen")]
211 #[test]
212 fn exports_generate_openapi_reference() {
213 let export = export_command_reference(&["generate".into(), "openapi".into()])
214 .expect("generate openapi export");
215
216 assert_eq!(export.command.name, "openapi");
217 assert!(
218 export
219 .command
220 .arguments
221 .iter()
222 .any(|argument| argument.id == "service" && argument.display == "--service <SERVICE>")
223 );
224 assert!(
225 export
226 .command
227 .arguments
228 .iter()
229 .any(|argument| argument.id == "all" && argument.display == "--all")
230 );
231 }
232}