use quote::ToTokens;
use crate::move_it_somewhere::find_reverse_clap_attribute;
#[derive(Debug, Default, PartialEq, Eq)]
pub struct FieldAttributes {
pub prefix: Option<String>,
pub field_name: String,
pub name: Option<String>,
pub delimiter: Option<String>,
pub is_option_ty: bool,
pub binary: bool,
pub transparent: bool,
pub flag: bool,
}
impl FieldAttributes {
pub const ARGUMENTS: [(&'static str, &'static str); 6] = [
("prefix", "sets up a prefix for argument (default: struct level prefix), example: `prefix=\"-\"` -{name} {value}"),
("name", "sets up a name for argument (default: field name), example: `name=\"test_argument\"` -test_argument {value}"),
("delimiter", "sets up a delimiter for argument (default: struct level delimiter), example: `delimiter=\"=\"` = --{name}={value}"),
("binary", "makes this field path for command binary"),
("transparent", "makes this field omit it's name, example (defalut: false): '{value}'"),
("flag", "makes this field omit it's value, example (default: false): '--{name}'"),
];
pub fn parse(input: &syn::Field) -> syn::Result<Self> {
let mut this = Self::default();
find_reverse_clap_attribute(&input.attrs)
.map(|attribute| attribute.parse_nested_meta(|meta| this.parse_meta(input, meta)))
.transpose()?;
this.field_name = input.ident.as_ref().map(|ident| ident.into_token_stream().to_string()).unwrap();
this.is_option_ty = Self::is_option(input);
Ok(this)
}
fn parse_meta(&mut self, input: &syn::Field, meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
if meta.path.is_ident("prefix") {
self.prefix = Some(meta.value()?.parse::<syn::LitStr>()?.value());
Ok(())
}
else if meta.path.is_ident("name") {
self.name = Some(meta.value()?.parse::<syn::LitStr>()?.value());
Ok(())
}
else if meta.path.is_ident("delimiter") {
self.delimiter = Some(meta.value()?.parse::<syn::LitStr>()?.value());
Ok(())
}
else if meta.path.is_ident("binary") {
self.binary = true;
Ok(())
}
else if meta.path.is_ident("transparent") {
self.transparent = true;
Ok(())
}
else if meta.path.is_ident("flag") {
if let syn::Type::Path(type_path) = &input.ty && type_path.path.is_ident("bool") {
self.flag = true;
Ok(())
} else {
Err(meta.error("flag argument can be used only on bool fields"))
}
}
else {
Err(meta.error(Self::unknown_argument_error(meta.path.to_token_stream())))
}
}
fn is_option(field: &syn::Field) -> bool {
if let syn::Type::Path(type_path) = &field.ty
&& type_path.qself.is_none()
&& type_path.path.segments.len() == 1
{
let segment = &type_path.path.segments[0];
return segment.ident == "Option";
}
false
}
fn unknown_argument_error(argument: impl std::fmt::Display) -> String {
use std::fmt::Write;
let mut error_message = format!("Unknown argument: `{argument}`. Available arguments:\n");
Self::ARGUMENTS.iter().for_each(|(arg, help)| { let _ = writeln!(&mut error_message, "\t- {arg} - {help}"); });
error_message
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::{parse_quote, Field};
fn parse_field(input: Field) -> FieldAttributes {
FieldAttributes::parse(&input).expect("failed to parse field")
}
#[test]
fn test_default_name_and_no_attrs() {
let field: Field = parse_quote! { text: String };
let attrs = parse_field(field);
assert_eq!(attrs.field_name, "text");
assert_eq!(attrs.name, None);
assert!(!attrs.binary);
assert!(!attrs.transparent);
assert!(!attrs.flag);
assert!(!attrs.is_option_ty);
assert!(attrs.prefix.is_none());
assert!(attrs.delimiter.is_none());
}
#[test]
fn test_prefix_attribute() {
let field: Field = parse_quote! {
#[clap_reverse(prefix = "-")]
arg: String
};
let attrs = parse_field(field);
assert_eq!(attrs.prefix.as_deref(), Some("-"));
}
#[test]
fn test_name_attribute() {
let field: Field = parse_quote! {
#[clap_reverse(name = "renamed")]
arg: String
};
let attrs = parse_field(field);
assert_eq!(attrs.name, Some("renamed".to_string()));
}
#[test]
fn test_delimiter_attribute() {
let field: Field = parse_quote! {
#[clap_reverse(delimiter = "=")]
arg: String
};
let attrs = parse_field(field);
assert_eq!(attrs.delimiter.as_deref(), Some("="));
}
#[test]
fn test_binary_attribute() {
let field: Field = parse_quote! {
#[clap_reverse(binary)]
path: String
};
let attrs = parse_field(field);
assert!(attrs.binary);
}
#[test]
fn test_transparent_attribute() {
let field: Field = parse_quote! {
#[clap_reverse(transparent)]
value: String
};
let attrs = parse_field(field);
assert!(attrs.transparent);
}
#[test]
fn test_flag_attribute_on_bool() {
let field: Field = parse_quote! {
#[clap_reverse(flag)]
verbose: bool
};
let attrs = parse_field(field);
assert!(attrs.flag);
}
#[test]
fn test_flag_attribute_on_non_bool_errors() {
let field: Field = parse_quote! {
#[clap_reverse(flag)]
count: u32
};
let result = FieldAttributes::parse(&field);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("flag argument can be used only on bool fields"));
}
#[test]
fn test_is_option_type() {
let field: Field = parse_quote! { opt: Option<String> };
let attrs = parse_field(field);
assert!(attrs.is_option_ty);
}
#[test]
fn test_unknown_attribute_errors() {
let field: Field = parse_quote! {
#[clap_reverse(unknown_attr)]
data: String
};
let result = FieldAttributes::parse(&field);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Unknown argument"));
assert!(err.contains("Available arguments:"));
assert!(err.contains("prefix"));
assert!(err.contains("flag"));
}
}