use clap::{Arg, ArgAction, ArgMatches, Command, command, value_parser};
use schemars::{SchemaGenerator, generate::SchemaSettings};
use serde::{Deserialize, Deserializer};
use serde_json::{Value, json};
use crate::settings::AppConfig;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum CommandFixedType {
String,
}
#[derive(Debug, Deserialize)]
struct CommandType {
#[expect(unused)]
#[serde(rename = "type")]
ctype: CommandFixedType,
#[serde(rename = "const")]
name: String,
}
#[derive(Debug, Deserialize)]
struct CommandProps {
#[serde(flatten)]
props: serde_json::Map<String, Value>,
command_type: CommandType,
}
#[derive(Debug, Deserialize)]
struct CommandDef {
properties: CommandProps,
required: Option<Vec<String>>,
}
fn add_args_from_root_schema(cmd: Command, schema: Value, defaults: &Value) -> Command {
let mut cmd = cmd;
let Value::Object(mut props) = schema else {
panic!("Invalid root schema is not of type object");
};
if let Some(command) = props.remove("command") {
let Value::Object(mut command_obj) = command else {
panic!("Invalid command is not object: {command}");
};
let Some(command_options) = command_obj.remove("oneOf") else {
panic!("Invalid command does not have oneOf attribute: {command_obj:?}");
};
let Value::Array(command_options) = command_options else {
panic!("Invalid command options is not array: {command_options}");
};
for command_def in command_options {
let command_def: CommandDef =
serde_json::from_value(command_def).expect("invalid command definition");
let command_type = command_def.properties.command_type.name.replace("_", "-");
let subcmd = add_args_from_schema(
Command::new(&command_type).about(&command_type),
command_def.properties.props,
&defaults.get("command").cloned().unwrap_or(Value::Null),
None,
Some("command"),
&command_def.required.unwrap_or_default(),
false,
);
cmd = cmd.subcommand(subcmd);
}
}
add_args_from_schema(cmd, props, defaults, None, None, &[], true)
}
fn deserialize_arg_type<'de, D>(deserializer: D) -> Result<ArgType, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum UnionType {
Str(ArgType),
Vec(Vec<Value>),
}
match UnionType::deserialize(deserializer)? {
UnionType::Str(arg_type) => Ok(arg_type),
UnionType::Vec(v) => {
if v.len() != 2 || v[1] != "null" {
Err(serde::de::Error::custom(
"arg types only support the second type null",
))
} else {
Ok(serde_json::from_value(v[0].clone()).map_err(serde::de::Error::custom)?)
}
}
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
enum ArgType {
String,
Integer,
Boolean,
Object,
Null,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
enum ArgFormat {
Uint,
Uint8,
Uint16,
}
#[derive(Debug, Deserialize)]
struct ArgDef {
#[serde(rename = "type", deserialize_with = "deserialize_arg_type")]
arg_type: ArgType,
properties: Option<serde_json::Map<String, Value>>,
required: Option<Vec<String>>,
format: Option<ArgFormat>,
}
fn add_args_from_schema(
cmd: Command,
props: serde_json::map::Map<String, Value>,
defaults: &Value,
flag_prefix: Option<&str>,
id_prefix: Option<&str>,
required: &[String],
global: bool,
) -> Command {
let mut cmd = cmd;
for (key, subschema) in props {
let flag_name = if let Some(prefix) = flag_prefix {
format!("{}-{}", prefix, key.replace('_', "-"))
} else {
key.replace('_', "-")
};
let arg_id = if let Some(prefix) = id_prefix {
format!("{}__{}", prefix, key)
} else {
key.clone()
};
let default_val = defaults.get(&key).cloned().unwrap_or(Value::Null);
let arg_def: ArgDef =
serde_json::from_value(subschema).expect("Invalid argument definition");
if arg_def.arg_type == ArgType::Object {
cmd = add_args_from_schema(
cmd,
arg_def
.properties
.expect("properties of type object should exist"),
&default_val,
Some(&flag_name),
Some(&arg_id),
arg_def.required.as_deref().unwrap_or(&[]),
global,
);
} else {
let arg = setup_arg(
arg_def,
required.contains(&key),
global,
flag_name,
arg_id,
default_val,
);
cmd = cmd.arg(arg);
}
}
cmd
}
fn setup_arg(
arg_def: ArgDef,
required: bool,
global: bool,
flag_name: String,
arg_id: String,
default_val: Value,
) -> Arg {
let mut arg = Arg::new(arg_id)
.long(&flag_name)
.value_name(flag_name.to_uppercase().replace("-", "_"))
.global(global);
if global {
arg = arg.display_order(0);
arg = arg.help_heading("GLOBAL");
} else {
arg = arg.display_order(1);
}
if required && default_val.is_null() && arg_def.arg_type != ArgType::Boolean {
arg = arg.required(true);
}
match arg_def.arg_type {
ArgType::Integer => {
match arg_def.format {
Some(ArgFormat::Uint) => {
arg = arg.value_parser(value_parser!(usize));
}
Some(ArgFormat::Uint8) => {
arg = arg.value_parser(value_parser!(u8));
}
Some(ArgFormat::Uint16) => {
arg = arg.value_parser(value_parser!(u16));
}
None => {
arg = arg.value_parser(value_parser!(i32));
}
}
if !default_val.is_null() {
arg = arg.default_value(default_val.to_string());
}
}
ArgType::String => {
if !default_val.is_null() {
arg = arg.default_value(default_val.as_str().unwrap().to_string());
}
}
ArgType::Boolean => {
arg = arg.action(ArgAction::SetTrue);
if default_val == Value::Bool(true) {
arg = arg.default_value("true");
}
}
_ => {
panic!("invalid value_type: {arg_def:?}")
}
}
arg
}
fn insert_composite_value(key: &str, value: Value, cli_json: &mut Value) {
let tokens_split: Vec<&str> = key.split("__").collect();
let mut cursor = cli_json.as_object_mut().unwrap();
for part in &tokens_split[..tokens_split.len() - 1] {
cursor = cursor
.entry(part.to_string())
.or_insert_with(|| json!({}))
.as_object_mut()
.unwrap();
}
cursor.insert(tokens_split.last().unwrap().to_string(), json!(value));
}
fn add_arg_matches_to_json(matches: &ArgMatches, cli_json: &mut Value) {
for id in matches.ids() {
let value = if let Ok(Some(str_value)) = matches.try_get_one::<String>(id.as_str()) {
json!(str_value)
} else if let Ok(Some(bool_value)) = matches.try_get_one::<bool>(id.as_str()) {
json!(bool_value)
} else if let Ok(Some(int_value)) = matches.try_get_one::<usize>(id.as_str()) {
json!(int_value)
} else if let Ok(Some(int_value)) = matches.try_get_one::<u8>(id.as_str()) {
json!(int_value)
} else if let Ok(Some(int_value)) = matches.try_get_one::<u16>(id.as_str()) {
json!(int_value)
} else {
panic!("Unknown arg value type: {id}");
};
insert_composite_value(id.as_str(), value, cli_json);
}
}
pub fn setup_from_schema(base_config: serde_json::Value) -> anyhow::Result<Value> {
let mut schema_settings = SchemaSettings::default();
schema_settings.inline_subschemas = true;
let mut schema = SchemaGenerator::new(schema_settings)
.into_root_schema_for::<AppConfig>()
.to_value();
let schema = schema
.as_object_mut()
.expect("schema is not an object")
.remove("properties")
.expect("missing properties attribute");
let mut cmd = command!();
cmd = add_args_from_root_schema(cmd, schema, &base_config);
let matches = cmd.get_matches();
let mut cli_json = json!({});
add_arg_matches_to_json(&matches, &mut cli_json);
if let Some((command_name, submatches)) = matches.subcommand() {
add_arg_matches_to_json(submatches, &mut cli_json);
insert_composite_value(
"command__command_type",
json!(command_name.replace("-", "_")),
&mut cli_json,
);
}
Ok(cli_json)
}