use clap::{Arg, Command, CommandFactory};
use rigg_core::registry;
use rigg_core::resources::ResourceKind;
use crate::cli::Cli;
pub fn cli_reference_markdown() -> String {
let mut root = Cli::command();
root.build();
let globals: Vec<Arg> = root
.get_arguments()
.filter(|a| a.is_global_set() && !is_builtin(a) && !a.is_hide_set())
.cloned()
.collect();
let mut out = String::new();
out.push_str("# CLI reference\n\n");
out.push_str("<!-- generated:cli-reference:start -->\n");
out.push_str(
"Generated by `rigg dev cli-reference` — do not edit by hand.\n\
Every command, argument and option below is read straight off the\n\
binary's own command tree; hidden developer commands are omitted.\n\n",
);
emit_command(&mut root, "rigg", &globals, &mut out);
out.push_str("<!-- generated:cli-reference:end -->\n");
out
}
fn is_builtin(arg: &Arg) -> bool {
matches!(arg.get_id().as_str(), "help" | "version")
}
fn emit_command(cmd: &mut Command, path: &str, globals: &[Arg], out: &mut String) {
let is_root = !path.contains(' ');
out.push_str(&format!("## {path}\n\n"));
if let Some(text) = cmd
.get_long_about()
.or_else(|| cmd.get_about())
.map(ToString::to_string)
{
let text = text.trim();
if !text.is_empty() {
out.push_str(text);
out.push_str("\n\n");
}
}
for line in cmd.render_usage().to_string().lines() {
let line = line.trim();
if !line.is_empty() {
out.push_str(&format!("`{line}`\n\n"));
}
}
let positionals: Vec<Arg> = cmd
.get_arguments()
.filter(|a| a.is_positional() && !a.is_hide_set())
.cloned()
.collect();
if !positionals.is_empty() {
out.push_str("### Arguments\n\n");
out.push_str("| Argument | Required | Description |\n|---|---|---|\n");
for arg in &positionals {
out.push_str(&format!(
"| `{}` | {} | {} |\n",
positional_name(arg),
if arg.is_required_set() { "yes" } else { "no" },
help_of(arg),
));
}
out.push('\n');
}
let options: Vec<Arg> = cmd
.get_arguments()
.filter(|a| {
!a.is_positional()
&& !a.is_hide_set()
&& !is_builtin(a)
&& (is_root || !a.is_global_set())
})
.cloned()
.collect();
if !options.is_empty() {
out.push_str(if is_root {
"### Global options\n\nAccepted by every command.\n\n"
} else {
"### Options\n\n"
});
out.push_str(&options_table(&options));
}
if !is_root && !globals.is_empty() {
out.push_str("Also accepts the [global options](#global-options).\n\n");
}
let children: Vec<String> = cmd
.get_subcommands()
.filter(|c| !c.is_hide_set() && c.get_name() != "help")
.map(|c| c.get_name().to_string())
.collect();
if !children.is_empty() {
out.push_str("### Subcommands\n\n");
for name in &children {
let child_path = format!("{path} {name}");
let summary = cmd
.get_subcommands()
.find(|c| c.get_name() == name.as_str())
.and_then(|c| c.get_about())
.map(|a| cell(&a.to_string()))
.unwrap_or_default();
let link = format!("[`{child_path}`](#{})", slug(&child_path));
if summary.is_empty() {
out.push_str(&format!("- {link}\n"));
} else {
out.push_str(&format!("- {link} — {summary}\n"));
}
}
out.push('\n');
}
for name in children {
let child_path = format!("{path} {name}");
let child = cmd
.get_subcommands_mut()
.find(|c| c.get_name() == name.as_str())
.expect("subcommand still present");
emit_command(child, &child_path, globals, out);
}
}
fn options_table(options: &[Arg]) -> String {
let mut out =
String::from("| Option | Value | Default | Env | Description |\n|---|---|---|---|---|\n");
for arg in options {
let mut names = Vec::new();
if let Some(long) = arg.get_long() {
names.push(format!("`--{long}`"));
}
if let Some(short) = arg.get_short() {
names.push(format!("`-{short}`"));
}
let value = if arg.get_action().takes_values() {
arg.get_value_names()
.map(|names| {
names
.iter()
.map(|n| format!("`<{n}>`"))
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_else(|| format!("`<{}>`", arg.get_id().as_str().to_uppercase()))
} else {
String::new()
};
let default = if arg.get_action().takes_values() {
arg.get_default_values()
.iter()
.map(|v| format!("`{}`", v.to_string_lossy()))
.collect::<Vec<_>>()
.join(", ")
} else {
String::new()
};
let env = arg
.get_env()
.map(|e| format!("`{}`", e.to_string_lossy()))
.unwrap_or_default();
out.push_str(&format!(
"| {} | {} | {} | {} | {} |\n",
names.join(", "),
value,
default,
env,
help_of(arg),
));
}
out.push('\n');
out
}
fn positional_name(arg: &Arg) -> String {
let name = arg
.get_value_names()
.and_then(|n| n.first().map(|n| n.to_string()))
.unwrap_or_else(|| arg.get_id().as_str().to_uppercase());
if arg.is_required_set() {
format!("<{name}>")
} else {
format!("[{name}]")
}
}
fn help_of(arg: &Arg) -> String {
arg.get_help()
.map(|h| cell(&h.to_string()))
.unwrap_or_default()
}
fn cell(text: &str) -> String {
let flat: String = text.split_whitespace().collect::<Vec<_>>().join(" ");
let mut out = String::with_capacity(flat.len());
let mut in_code = false;
for ch in flat.chars() {
match ch {
'`' => {
in_code = !in_code;
out.push('`');
}
'|' => out.push_str("\\|"),
'<' if !in_code => out.push_str("<"),
'>' if !in_code => out.push_str(">"),
c => out.push(c),
}
}
out
}
pub fn slug(heading: &str) -> String {
heading
.to_lowercase()
.chars()
.filter_map(|c| match c {
' ' => Some('-'),
c if c.is_ascii_alphanumeric() || c == '-' => Some(c),
_ => None,
})
.collect()
}
pub fn infra_table_markdown() -> String {
let mut out = String::new();
for kind in ResourceKind::all() {
let refs = registry::infra_refs(*kind);
if refs.is_empty() {
continue;
}
out.push_str(&format!("### {}\n\n", kind.directory_name()));
out.push_str(
"| Path | Binding type | Form | Only for `@odata.type` |\n|---|---|---|---|\n",
);
for r in refs {
out.push_str(&format!(
"| `{}` | {} | `{:?}` | {} |\n",
r.path,
r.form.binding_type_label(),
r.form,
r.only_odata_type
.map(|t| format!("`{t}`"))
.unwrap_or_else(|| "—".to_string()),
));
}
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_reference_covers_the_visible_tree_and_links_resolve() {
let md = cli_reference_markdown();
assert!(md.starts_with("# CLI reference\n"));
assert!(md.contains("## rigg\n"));
assert!(md.contains("### Global options\n"));
assert!(md.contains("## rigg push\n"));
assert!(md.contains("[`rigg push`](#rigg-push)"));
assert!(!md.contains("## rigg dev"));
assert!(!md.contains('\u{1b}'));
}
#[test]
fn infra_table_has_a_section_per_kind_with_refs() {
let md = infra_table_markdown();
assert!(md.contains("### data-sources\n"));
assert!(md.contains("| `credentials.connectionString` | storage |"));
assert!(!md.contains("### synonym-maps"));
}
#[test]
fn slugs_follow_github_rules() {
assert_eq!(slug("rigg env add"), "rigg-env-add");
assert_eq!(slug("Global options"), "global-options");
assert_eq!(
slug("`x-rigg-ref`: what it does"),
"x-rigg-ref-what-it-does"
);
}
}