use clap::{Arg, ArgAction, Command, CommandFactory, ValueHint};
use serde::Serialize;
use serde_json::json;
use crate::error::{CliError, Result};
#[derive(clap::Args, Debug)]
pub struct GuiSchemaArgs {}
pub fn run(_args: GuiSchemaArgs) -> Result<u8> {
let cmd = crate::Cli::command();
let schema = build_schema(&cmd);
let s = serde_json::to_string(&schema)
.map_err(|e| CliError::UsageError(format!("gui-schema serialize: {e}")))?;
println!("{s}");
Ok(0)
}
#[derive(Serialize)]
struct Subcommand {
name: String,
flags: Vec<Flag>,
positionals: Vec<Positional>,
}
#[derive(Serialize)]
struct Flag {
name: String,
required: bool,
kind: &'static str,
choices: Option<Vec<String>>,
}
#[derive(Serialize)]
struct Positional {
name: String,
required: bool,
repeating: bool,
}
fn build_schema(cmd: &Command) -> serde_json::Value {
let mut subs: Vec<Subcommand> = Vec::new();
for sub in cmd.get_subcommands() {
let name = sub.get_name().to_string();
if name == "help" || name == "gui-schema" {
continue;
}
let mut flags: Vec<Flag> = Vec::new();
let mut positionals: Vec<Positional> = Vec::new();
for arg in sub.get_arguments() {
if arg.is_positional() {
positionals.push(positional_from_arg(arg));
} else {
let id = arg.get_id().as_str();
if id == "help" || id == "version" {
continue;
}
flags.push(flag_from_arg(arg));
}
}
subs.push(Subcommand {
name,
flags,
positionals,
});
}
json!({
"version": 1,
"cli": "mk",
"subcommands": subs,
})
}
fn flag_from_arg(arg: &Arg) -> Flag {
let long = arg
.get_long()
.map(|s| format!("--{s}"))
.unwrap_or_else(|| format!("--{}", arg.get_id().as_str().replace('_', "-")));
let required = arg.is_required_set();
let (kind, choices) = classify(arg);
Flag {
name: long,
required,
kind,
choices,
}
}
fn positional_from_arg(arg: &Arg) -> Positional {
let name = arg.get_id().as_str().to_string();
let required = arg.is_required_set();
let repeating = matches!(arg.get_action(), ArgAction::Append | ArgAction::Count)
|| arg.get_num_args().is_some_and(|n| n.max_values() > 1);
Positional {
name,
required,
repeating,
}
}
fn classify(arg: &Arg) -> (&'static str, Option<Vec<String>>) {
if matches!(arg.get_action(), ArgAction::SetTrue | ArgAction::SetFalse) {
return ("boolean", None);
}
let choices: Vec<String> = arg
.get_possible_values()
.iter()
.map(|pv| pv.get_name().to_string())
.collect();
if !choices.is_empty() {
return ("dropdown", Some(choices));
}
match arg.get_value_hint() {
ValueHint::FilePath
| ValueHint::DirPath
| ValueHint::AnyPath
| ValueHint::ExecutablePath => return ("path", None),
_ => {}
}
if arg.get_value_parser().type_id() == std::any::TypeId::of::<std::path::PathBuf>() {
return ("path", None);
}
("text", None)
}