use crate::cli::Cli;
use crate::guard::MUTATING;
use clap::CommandFactory;
use clap_complete::Shell;
use elasticctl_core::Result;
use serde_json::{Value, json};
fn invoked_name() -> String {
std::env::args_os()
.next()
.map(std::path::PathBuf::from)
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))
.unwrap_or_else(|| "elasticctl".to_string())
}
pub fn completion(shell: Shell) -> Result<()> {
let mut cmd = Cli::command();
clap_complete::generate(shell, &mut cmd, invoked_name(), &mut std::io::stdout());
Ok(())
}
fn arg(a: &clap::Arg) -> Value {
json!({
"name": a.get_id().as_str(),
"required": a.is_required_set(),
"takes_value": a.get_num_args().map(|n| n.takes_values()).unwrap_or(false),
"help": a.get_help().map(|h| h.to_string()),
})
}
fn describe(cmd: &clap::Command, path: &str) -> Value {
let args: Vec<Value> = cmd
.get_arguments()
.filter(|a| !a.is_hide_set())
.map(arg)
.collect();
let subcommands: Vec<Value> = cmd
.get_subcommands()
.map(|sub| {
let sub_path = format!("{path} {}", sub.get_name());
describe(sub, &sub_path)
})
.collect();
json!({
"name": cmd.get_name(),
"about": cmd.get_about().map(|a| a.to_string()),
"mutates": MUTATING.contains(&path),
"args": args,
"subcommands": subcommands,
})
}
pub fn command_tree() -> Result<Value> {
let cmd = Cli::command();
let global_args: Vec<Value> = cmd
.get_arguments()
.filter(|a| !a.is_hide_set())
.map(arg)
.collect();
let commands: Vec<Value> = cmd
.get_subcommands()
.map(|sub| describe(sub, sub.get_name()))
.collect();
Ok(json!({
"name": "elasticctl",
"version": env!("CARGO_PKG_VERSION"),
"global_args": global_args,
"commands": commands,
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_tree_records_the_global_flags_on_the_root() {
let tree = command_tree().unwrap();
let names: Vec<&str> = tree["global_args"]
.as_array()
.unwrap()
.iter()
.map(|a| a["name"].as_str().unwrap())
.collect();
for expected in [
"profile", "config", "space", "json", "format", "fields", "out", "yes", "timeout",
"debug",
] {
assert!(
names.contains(&expected),
"global_args must list {expected}: {names:?}"
);
}
}
fn mutating_paths(value: &Value, path: &str) -> Vec<String> {
let mut found = Vec::new();
if value["mutates"] == true {
found.push(path.to_string());
}
for sub in value["subcommands"]
.as_array()
.map(|a| a.as_slice())
.unwrap_or(&[])
{
let name = sub["name"].as_str().unwrap();
let sub_path = if path.is_empty() {
name.to_string()
} else {
format!("{path} {name}")
};
found.extend(mutating_paths(sub, &sub_path));
}
found
}
#[test]
fn the_mutating_set_is_exactly_the_declared_paths() {
let tree = command_tree().unwrap();
let mut actual: Vec<String> = tree["commands"]
.as_array()
.unwrap()
.iter()
.flat_map(|c| mutating_paths(c, c["name"].as_str().unwrap()))
.collect();
actual.sort();
let mut expected = [
"rules enable",
"rules disable",
"rules delete",
"rules import",
"rules prebuilt install",
"exceptions delete",
"exceptions import",
"state push",
"config init",
"alerts ack",
"alerts open",
"alerts close",
"alerts tag",
"alerts assign",
"cases create",
"cases close",
"cases open",
"cases delete",
"cases attach",
"cases comment",
"data-views import",
"data-views delete",
"data-views default set",
"data-views default unset",
"dashboards import",
"dashboards delete",
"dashboards bundle import",
"fleet agent-policies import",
"fleet agent-policies delete",
];
expected.sort();
assert_eq!(actual, expected, "the set of mutating commands drifted");
}
}