pub(crate) mod value_enum;
use std::ffi::OsString;
use clap::{Arg, ArgMatches, Args, Command, FromArgMatches, parser::MatchesError};
use crate::{
Configuration,
internal::{
ChangeVerb, CongenChange, Description, FieldDescription, ListDescription, ListKey,
ListKeyType, ListValue, ListVerb,
},
};
pub use value_enum::ValueEnumConfiguration;
pub struct CongenClap<T: Configuration> {
change: T::CongenChange,
}
impl<T: Configuration> CongenClap<T> {
pub fn into_change(self) -> T::CongenChange {
self.change
}
pub fn create_cmd(cmd_name: impl Into<clap::builder::Str>) -> Command {
let for_update = false;
Command::new(cmd_name)
.subcommands(actionable_subcommands(
&T::description("__clap"),
for_update,
))
.subcommand_required(true)
}
}
fn field_value_arg(field: &FieldDescription) -> Arg {
let mut arg = Arg::new("value").value_name(field.type_name.to_uppercase());
if let Some(clap) = &field.clap_data {
arg = arg.value_hint(clap.value_hint);
if clap.allow_hyphen_values {
arg = arg.allow_hyphen_values(true);
}
if clap.allow_negative_numbers {
arg = arg.allow_negative_numbers(true);
}
if let Some(parser) = &clap.value_parser {
arg = arg.value_parser(parser.clone());
}
}
arg
}
fn actionable_subcommands(
description: &Description,
for_update: bool,
) -> impl Iterator<Item = Command> {
description
.actionable_fields()
.into_iter()
.map(move |mut actionable| {
let field_name = actionable.path.make_contiguous().join(".");
let mut field_command = Command::new(&field_name).subcommand_required(!for_update);
match &actionable.description {
Description::Field(field) => {
let mut set = Command::new("set");
if !field.is_flag {
set = set.arg(field_value_arg(field).required(!for_update));
}
field_command = field_command.subcommand(set);
}
Description::List(list) => {
let key_arg = Arg::new("key").value_name("AT").required(!for_update);
let key_arg = match list.key_type {
ListKeyType::String => key_arg,
ListKeyType::Signed => key_arg
.value_parser(clap::value_parser!(i64))
.allow_negative_numbers(true),
ListKeyType::Unsigned => key_arg.value_parser(clap::value_parser!(u64)),
};
field_command = field_command
.subcommand(Command::new("empty"))
.subcommand(Command::new("remove").arg(key_arg.clone()));
match list.inner_desc.as_ref() {
Description::Field(inner_field) => {
let value_arg = field_value_arg(inner_field).required(!for_update);
let append_command = if list.append_requires_key {
Command::new("append")
.arg(key_arg.clone())
.arg(value_arg.clone())
} else {
Command::new("append").arg(value_arg.clone())
};
field_command = field_command
.subcommand(append_command)
.subcommand(Command::new("update").arg(key_arg).arg(value_arg));
}
Description::Composit(_) | Description::List(_) => {
let append_command = if list.append_requires_key {
Command::new("append")
.arg(key_arg.clone())
.subcommands(actionable_subcommands(
list.inner_desc.as_ref(),
for_update,
))
.subcommand_required(!for_update)
} else {
Command::new("append")
.subcommands(actionable_subcommands(
list.inner_desc.as_ref(),
for_update,
))
.subcommand_required(!for_update)
};
field_command = field_command.subcommand(append_command).subcommand(
Command::new("update")
.arg(key_arg)
.subcommands(actionable_subcommands(
list.inner_desc.as_ref(),
for_update,
))
.subcommand_required(!for_update),
);
}
}
}
_ => (),
}
if actionable.description.has_default() {
field_command = field_command.subcommand(Command::new("use-default"));
}
if actionable.description.allow_unset() {
field_command = field_command.subcommand(Command::new("unset"));
}
field_command
})
}
impl<T: Configuration> Args for CongenClap<T> {
fn augment_args(cmd: Command) -> Command {
let for_update = false;
cmd.subcommands(actionable_subcommands(
&T::description("__clap"),
for_update,
))
.subcommand_required(true)
}
fn augment_args_for_update(cmd: Command) -> Command {
let for_update = true;
cmd.subcommands(actionable_subcommands(
&T::description("__clap"),
for_update,
))
.subcommand_required(true)
}
}
impl<T: Configuration> FromArgMatches for CongenClap<T>
where
T::CongenChange: Sized,
{
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
let mut res = Self {
change: CongenChange::empty(),
};
res.update_from_arg_matches(matches)?;
Ok(res)
}
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
let description = T::description("");
let (field_path, verb) = get_verb_from_cmd(matches, &description)?;
let change = T::CongenChange::from_path_and_verb(field_path, verb)
.expect("Failed to create change for path");
self.change.apply_change(change);
Ok(())
}
}
fn get_verb_from_cmd<'a>(
cmd_matches: &'a ArgMatches,
description: &Description,
) -> Result<(impl Iterator<Item = &'a str>, ChangeVerb), clap::error::Error> {
let Some((field_name, field_cmd)) = cmd_matches.subcommand() else {
return Err(clap::Error::new(clap::error::ErrorKind::MissingSubcommand));
};
let field_path = field_name.split(".");
let Some(field_desc) = description.actionable_field(field_path.clone()) else {
return Err(clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
"invalid path",
));
};
let Some((verb_name, verb_cmd)) = field_cmd.subcommand() else {
return Err(clap::Error::raw(
clap::error::ErrorKind::MissingSubcommand,
"missing verb",
));
};
let verb = match (verb_name, field_desc) {
("unset", _) => ChangeVerb::Unset,
("use-default", _) => ChangeVerb::UseDefault,
("set", Description::Field(_)) | ("set", Description::Composit(_)) => {
match verb_cmd.try_get_raw("value") {
Ok(Some(mut raw_value)) => {
let Some(value) = raw_value.next() else {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooFewValues,
"missing value",
));
};
if raw_value.next().is_some() {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooManyValues,
"to many values",
));
}
ChangeVerb::Set(value.to_owned())
}
Err(MatchesError::UnknownArgument { .. }) => ChangeVerb::SetFlag,
Ok(None) => {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooFewValues,
"missing value",
));
}
Err(_err) => return Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)),
}
}
(verb_name, Description::List(list_desc)) => {
get_verb_for_list(verb_name, verb_cmd, &list_desc)?.into()
}
(verb, desc) => {
return Err(clap::Error::raw(
clap::error::ErrorKind::InvalidSubcommand,
format!("invalid verb ({verb}) for {desc:?}"),
));
}
};
Ok((field_path, verb))
}
fn get_value(verb_cmd: &ArgMatches) -> Result<OsString, clap::Error> {
let missing_value_error =
|| clap::Error::raw(clap::error::ErrorKind::TooFewValues, "missing value");
let mut raw_values = verb_cmd.get_raw("value").ok_or_else(missing_value_error)?;
let raw = raw_values.next().ok_or_else(missing_value_error)?;
if raw_values.next().is_some() {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooManyValues,
"too many values",
));
}
Ok(raw.to_owned())
}
fn get_verb_for_list(
verb_name: &str,
verb_cmd: &ArgMatches,
list_desc: &ListDescription,
) -> Result<ListVerb, clap::Error> {
match (verb_name, list_desc.inner_desc.as_ref()) {
("remove", _) => {
let Some(key) = get_key(verb_cmd, list_desc) else {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooFewValues,
"missing key",
));
};
Ok(ListVerb::Remove { key })
}
("empty", _) => Ok(ListVerb::Empty),
("append", Description::Field(_)) => {
let key = if list_desc.append_requires_key {
let Some(key) = get_key(verb_cmd, list_desc) else {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooFewValues,
"missing key",
));
};
Some(key)
} else {
None
};
let new_value = get_value(verb_cmd)?;
Ok(ListVerb::Append {
key,
new_value: new_value.into(),
})
}
("update", Description::Field(_)) => {
let Some(key) = get_key(verb_cmd, list_desc) else {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooFewValues,
"missing key",
));
};
let updated_value = get_value(verb_cmd)?;
Ok(ListVerb::Update {
key,
updated_value: updated_value.into(),
})
}
("append", _) => {
let key = if list_desc.append_requires_key {
let Some(key) = get_key(verb_cmd, list_desc) else {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooFewValues,
"missing key",
));
};
Some(key)
} else {
None
};
let (path, verb) = get_verb_from_cmd(verb_cmd, list_desc.inner_desc.as_ref())?;
let path: Vec<String> = path.map(|p| p.to_string()).collect();
let verb = Box::new(verb);
Ok(ListVerb::Append {
key,
new_value: ListValue::WithPath(path, verb),
})
}
("update", _) => {
let Some(key) = get_key(verb_cmd, list_desc) else {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooFewValues,
"missing key",
));
};
let (path, verb) = get_verb_from_cmd(verb_cmd, list_desc.inner_desc.as_ref())?;
let path: Vec<String> = path.map(|p| p.to_string()).collect();
let verb = Box::new(verb);
Ok(ListVerb::Update {
key,
updated_value: ListValue::WithPath(path, verb),
})
}
(verb, _) => Err(clap::Error::raw(
clap::error::ErrorKind::InvalidSubcommand,
format!("invalid verb ({verb}) for {list_desc:?}"),
)),
}
}
fn get_key(arg_matches: &ArgMatches, field_desc: &ListDescription) -> Option<ListKey> {
match field_desc.key_type {
ListKeyType::String => arg_matches
.get_one::<String>("key")
.map(|key| ListKey::Stringy(key.clone())),
ListKeyType::Signed => arg_matches
.get_one::<i64>("key")
.map(|key| ListKey::Signed(*key)),
ListKeyType::Unsigned => arg_matches
.get_one::<u64>("key")
.map(|key| ListKey::Unsigned(*key)),
}
}