use clap::CommandFactory;
use clap::builder::PossibleValue;
use serde_json::{Value, json};
use super::args::Cli;
const ALIAS_PREFERRED: &[(&str, &str)] = &[
("quote", "market quote"),
("historical", "market historical"),
("profile", "company profile"),
("earnings", "calendar earnings"),
];
pub(super) fn schema_payload() -> Value {
let cmd = Cli::command();
let mut groups: Vec<String> = Vec::new();
let mut commands: Vec<Value> = Vec::new();
for sub in cmd.get_subcommands() {
let name = sub.get_name();
let children: Vec<_> = sub
.get_subcommands()
.filter(|c| c.get_name() != "help")
.collect();
if name == "help" && children.is_empty() {
continue;
}
if children.is_empty() {
let path = vec![name.to_owned()];
let preferred = preferred_path_for(name);
let api_key_required =
!matches!(name, "schema" | "commands" | "completions" | "doctor");
commands.push(leaf_to_json(sub, path, preferred, &[], api_key_required));
} else {
groups.push(name.to_owned());
for child in &children {
let child_name = child.get_name();
let path = vec![name.to_owned(), child_name.to_owned()];
let preferred = format!("{name} {child_name}");
let aliases = aliases_for(name, child_name);
let api_key_required = name != "help";
commands.push(leaf_to_json(
child,
path,
preferred,
&aliases,
api_key_required,
));
}
}
}
json!({
"schema_version": 3,
"binary": "fmp-agent",
"version": env!("CARGO_PKG_VERSION"),
"groups": groups,
"commands": commands,
})
}
fn leaf_to_json(
sub: &clap::Command,
path: Vec<String>,
preferred_path: String,
aliases: &[String],
api_key_required: bool,
) -> Value {
let name = sub.get_name().to_owned();
let about = sub.get_about().map(|s| s.to_string());
let long_about = sub.get_long_about().map(|s| s.to_string());
let args: Vec<Value> = sub
.get_arguments()
.filter(|a| !a.is_hide_set())
.map(|a| {
let kind = if a.is_positional() {
"positional"
} else {
"option"
};
let default_str = a.get_default_values().first().and_then(|v| v.to_str());
let default_val = default_str.map(|s| {
if let Ok(n) = s.parse::<u64>() {
json!(n)
} else {
json!(s)
}
});
let value_name = a
.get_value_names()
.and_then(|names| names.first())
.map(|n| json!(n.as_str()));
let help_text = a.get_help().map(|s| s.to_string());
let long_flag = a.get_long().map(|s| json!(s));
let short_flag = a.get_short().map(|c| json!(c.to_string()));
let parser = parser_hint(a, default_str);
let possible_vals: Option<Vec<Value>> = possible_values_json(&a.get_possible_values());
let multi = matches!(a.get_action(), clap::ArgAction::Append)
|| a.get_num_args().is_some_and(|r| r.max_values() > 1);
json!({
"name": a.get_id().as_str(),
"kind": kind,
"required": a.is_required_set(),
"default": default_val,
"value_name": value_name,
"help": help_text,
"long": long_flag,
"short": short_flag,
"parser": parser,
"possible_values": possible_vals,
"multi_value": multi,
})
})
.collect();
json!({
"name": name,
"path": path,
"about": about,
"long_about": long_about,
"args": args,
"aliases": aliases,
"preferred_path": preferred_path,
"api_key_required": api_key_required,
})
}
fn preferred_path_for(name: &str) -> String {
for &(alias, canonical) in ALIAS_PREFERRED {
if alias == name {
return canonical.to_owned();
}
}
name.to_owned()
}
fn aliases_for(group: &str, child: &str) -> Vec<String> {
let canonical = format!("{group} {child}");
for &(alias, preferred) in ALIAS_PREFERRED {
if preferred == canonical {
return vec![alias.to_owned()];
}
}
Vec::new()
}
fn parser_hint(a: &clap::Arg, default_str: Option<&str>) -> Value {
let action = a.get_action();
if matches!(action, clap::ArgAction::SetTrue | clap::ArgAction::SetFalse) {
return json!({ "hint": "bool" });
}
if matches!(action, clap::ArgAction::Count) {
return json!({ "hint": "count" });
}
if !a.get_possible_values().is_empty() {
return json!({ "hint": "enum" });
}
if default_str.is_some_and(|s| s.parse::<u64>().is_ok()) {
return json!({ "hint": "integer" });
}
json!({ "hint": "string" })
}
fn possible_values_json(values: &[PossibleValue]) -> Option<Vec<Value>> {
if values.is_empty() {
return None;
}
Some(
values
.iter()
.filter(|v| !v.is_hide_set())
.map(|v| {
let name = v.get_name();
let help = v.get_help().map(|s| s.to_string());
if let Some(help) = help {
json!({ "name": name, "help": help })
} else {
json!({ "name": name, "help": null })
}
})
.collect(),
)
}