use quote::ToTokens;
use crate::move_it_somewhere::find_reverse_clap_attribute;
#[derive(Debug, PartialEq, Eq)]
pub struct StructAttributes {
pub display: bool,
pub prefix: String,
pub delimiter: String,
pub binary: Option<String>,
}
impl Default for StructAttributes {
fn default() -> Self {
Self {
display: false,
prefix: "--".to_string(),
delimiter: " ".to_string(),
binary: None,
}
}
}
impl StructAttributes {
pub const ARGUMENTS: [(&'static str, &'static str); 4] = [
("display", "generates implementation of a std::fmt::Display"),
("prefix", "sets up a prefix for arguments (default: '--'), example: `prefix=\"-\"` -{name} {value}"),
("delimiter", "sets up a delimiter for arguments (default: ' '), example: `delimiter=\"=\"` = --{name}={value}"),
("binary", "sets up a static path for command binary, example: `binary=\"java\"`"),
];
pub fn parse(input: &syn::DeriveInput) -> syn::Result<Self> {
let mut this = Self::default();
find_reverse_clap_attribute(&input.attrs)
.map(|attribute| attribute.parse_nested_meta(|meta| this.parse_meta(meta)))
.transpose()?;
Ok(this)
}
fn parse_meta(&mut self, meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
if meta.path.is_ident("display") {
self.display = true;
Ok(())
}
else if meta.path.is_ident("prefix") {
self.prefix = meta.value()?.parse::<syn::LitStr>()?.value();
Ok(())
}
else if meta.path.is_ident("delimiter") {
self.delimiter = meta.value()?.parse::<syn::LitStr>()?.value();
Ok(())
}
else if meta.path.is_ident("binary") {
self.binary = Some(meta.value()?.parse::<syn::LitStr>()?.value());
Ok(())
}
else {
Err(meta.error(Self::unknown_argument_error(meta.path.to_token_stream())))
}
}
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;
fn parse_struct(input: syn::DeriveInput) -> StructAttributes {
StructAttributes::parse(&input).expect("failed to parse struct attributes")
}
#[test]
fn test_defaults_no_attrs() {
let input: syn::DeriveInput = parse_quote! {
struct Test { field: String }
};
let attrs = parse_struct(input);
assert!(!attrs.display);
assert_eq!(attrs.prefix, "--");
assert_eq!(attrs.delimiter, " ");
assert!(attrs.binary.is_none());
}
#[test]
fn test_display_attribute() {
let input: syn::DeriveInput = parse_quote! {
#[clap_reverse(display)]
struct Test { field: String }
};
let attrs = parse_struct(input);
assert!(attrs.display);
}
#[test]
fn test_prefix_attribute() {
let input: syn::DeriveInput = parse_quote! {
#[clap_reverse(prefix = "-")]
struct Test { field: String }
};
let attrs = parse_struct(input);
assert_eq!(attrs.prefix, "-");
}
#[test]
fn test_delimiter_attribute() {
let input: syn::DeriveInput = parse_quote! {
#[clap_reverse(delimiter = "=")]
struct Test { field: String }
};
let attrs = parse_struct(input);
assert_eq!(attrs.delimiter, "=");
}
#[test]
fn test_binary_attribute() {
let input: syn::DeriveInput = parse_quote! {
#[clap_reverse(binary = "echo")]
struct Test { field: String }
};
let attrs = parse_struct(input);
assert_eq!(attrs.binary.as_deref(), Some("echo"));
}
#[test]
fn test_multiple_attributes() {
let input: syn::DeriveInput = parse_quote! {
#[clap_reverse(display, prefix = "-", delimiter = "=", binary = "java")]
struct Test { field: String }
};
let attrs = parse_struct(input);
assert!(attrs.display);
assert_eq!(attrs.prefix, "-");
assert_eq!(attrs.delimiter, "=");
assert_eq!(attrs.binary.as_deref(), Some("java"));
}
#[test]
fn test_unknown_attribute_errors() {
let input: syn::DeriveInput = parse_quote! {
#[clap_reverse(unknown_attr)]
struct Test { field: String }
};
let result = StructAttributes::parse(&input);
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("binary"));
}
}