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",
"value-lists import",
"value-lists delete",
"value-lists init",
"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",
"fleet integration-policies import",
"fleet integration-policies delete",
];
expected.sort();
assert_eq!(actual, expected, "the set of mutating commands drifted");
}
}