vership 0.5.0

Multi-target release orchestrator
Documentation
use serde_json::{Value, json};

pub fn generate(cmd: &clap::Command) -> Value {
    let commands: Vec<Value> = cmd
        .get_subcommands()
        .map(|sub| command_to_json(sub, cmd.get_name()))
        .collect();

    json!({
        "tool": cmd.get_name(),
        "version": env!("CARGO_PKG_VERSION"),
        "description": "Multi-target release orchestrator",
        "commands": commands,
    })
}

fn command_to_json(cmd: &clap::Command, parent: &str) -> Value {
    let path = format!("{parent} {}", cmd.get_name());

    let args: Vec<Value> = cmd
        .get_arguments()
        .filter(|a| a.get_id().as_str() != "help" && a.get_id().as_str() != "version")
        .map(arg_to_json)
        .collect();

    let subcommands: Vec<Value> = cmd
        .get_subcommands()
        .map(|sub| command_to_json(sub, &path))
        .collect();

    let mut obj = json!({
        "name": cmd.get_name(),
        "path": path,
    });

    if let Some(about) = cmd.get_about() {
        obj["description"] = json!(about.to_string());
    }
    if !args.is_empty() {
        obj["arguments"] = json!(args);
    }
    if !subcommands.is_empty() {
        obj["subcommands"] = json!(subcommands);
    }
    obj
}

fn arg_to_json(arg: &clap::Arg) -> Value {
    let mut obj = serde_json::Map::new();
    let name = if arg.is_positional() {
        arg.get_id().as_str().to_string()
    } else {
        arg.get_long()
            .map(|l| format!("--{l}"))
            .unwrap_or_else(|| arg.get_id().as_str().to_string())
    };
    obj.insert("name".into(), json!(name));
    if let Some(help) = arg.get_help() {
        obj.insert("description".into(), json!(help.to_string()));
    }
    let is_bool = !arg.get_action().takes_values();
    obj.insert(
        "type".into(),
        json!(if is_bool { "bool" } else { "string" }),
    );
    Value::Object(obj)
}