use crate::{FieldAttributes, StructAttributes};
pub struct ClapReverseGenerator {
input: syn::DeriveInput,
struct_attributes: StructAttributes,
fields_attributes: Vec<FieldAttributes>,
}
impl ClapReverseGenerator {
pub fn parse(input: syn::DeriveInput) -> syn::Result<Self> {
let syn::Data::Struct(struct_data) = &input.data else {
return Err(syn::Error::new_spanned(input, "clap_reverse works only on structs"));
};
let struct_attributes = StructAttributes::parse(&input)?;
let fields_attributes = struct_data.fields.iter().map(FieldAttributes::parse).collect::<syn::Result<Vec<_>>>()?;
let static_binary_setted = struct_attributes.binary.is_some();
let dynamic_binary_setted = fields_attributes.iter().any(|attr| attr.binary);
let dynamic_binary_count = fields_attributes.iter().filter(|attr| attr.binary).count();
if !(static_binary_setted ^ dynamic_binary_setted) {
return Err(syn::Error::new_spanned(input, "`binary` argument must be setted on struct OR on one of the fields."));
}
if dynamic_binary_count > 1 {
return Err(syn::Error::new_spanned(input, "`binary` argument must be setted ONLY on ONE of the fields."));
}
Ok(Self {
input,
struct_attributes,
fields_attributes,
})
}
pub fn generate(&self) -> syn::Result<proc_macro2::TokenStream> {
let as_command_impl = self.as_command_impl();
let display_impl = self.display_impl();
Ok(quote::quote! {
#as_command_impl
#display_impl
})
}
fn as_command_impl(&self) -> proc_macro2::TokenStream {
let struct_name = &self.input.ident;
let binary = self.get_binary();
let command_arguments = self.get_command_arguments();
quote::quote! {
impl clap_reverse::AsCommand for #struct_name {
fn as_command(&self) -> std::process::Command {
let mut cmd = std::process::Command::new(#binary);
#(#command_arguments)*
cmd
}
}
}
}
fn get_binary(&self) -> proc_macro2::TokenStream {
if let Some(binary_str) = &self.struct_attributes.binary {
quote::quote! { #binary_str }
} else if let Some(attr) = self.fields_attributes.iter().find(|attr| attr.binary) {
let ident = syn::Ident::new(&attr.field_name, proc_macro2::Span::call_site());
quote::quote! { &self.#ident }
} else {
unreachable!("No binary field found. ClapReverseGenerator::parse ensures binary existence.");
}
}
fn get_command_arguments(&self) -> Vec<proc_macro2::TokenStream> {
self
.fields_attributes
.iter()
.filter(|attr| !attr.binary)
.map(|attr| {
let prefix = attr.prefix.as_ref().unwrap_or(&self.struct_attributes.prefix);
let name = attr.name.as_ref().unwrap_or(&attr.field_name);
let delimiter = attr.delimiter.as_ref().unwrap_or(&self.struct_attributes.delimiter);
let field_ident = syn::Ident::new(&attr.field_name, proc_macro2::Span::call_site());
if attr.flag {
quote::quote! {
if self.#field_ident {
cmd.arg(format!("{}{}", #prefix, #name));
}
}
} else if attr.transparent {
quote::quote! {
cmd.arg(format!("{}", &self.#field_ident));
}
} else if attr.is_option_ty {
quote::quote! {
if let Some(value) = self.#field_ident {
cmd.arg(format!("{}{}{}{}", #prefix, #name, #delimiter, value));
}
}
} else {
quote::quote! {
cmd.arg(format!("{}{}{}{}", #prefix, #name, #delimiter, &self.#field_ident));
}
}
})
.collect::<Vec<_>>()
}
fn display_impl(&self) -> Option<proc_macro2::TokenStream> {
let struct_name = &self.input.ident;
self.struct_attributes.display.then(|| quote::quote! {
impl std::fmt::Display for #struct_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use clap_reverse::AsCommand;
let command = self.as_command();
let stringified_command = command
.get_args()
.fold(command.get_program().to_string_lossy().into_owned(), |mut acc, arg| {
acc.push(' ');
acc.push_str(&arg.to_string_lossy());
acc
});
write!(f, "{stringified_command}")
}
}
})
}
}
impl std::fmt::Debug for ClapReverseGenerator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClapReverseGenerator")
.field("struct_attributes", &self.struct_attributes)
.field("fields_attributes", &self.fields_attributes)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::{parse_quote, DeriveInput};
fn parse_ok(input: DeriveInput) -> ClapReverseGenerator {
ClapReverseGenerator::parse(input).expect("parse should succeed")
}
#[test]
fn parse_struct_with_static_binary() {
let input: DeriveInput = parse_quote! {
#[clap_reverse(binary = "echo")]
struct Test {
#[clap_reverse(flag)]
verbose: bool,
#[clap_reverse(name = "file")]
path: String,
}
};
let generator = parse_ok(input);
assert_eq!(generator.struct_attributes.binary.as_deref(), Some("echo"));
assert_eq!(generator.fields_attributes.len(), 2);
assert!(generator.fields_attributes.iter().any(|f| f.flag));
}
#[test]
fn parse_struct_with_dynamic_binary() {
let input: DeriveInput = parse_quote! {
struct Test {
#[clap_reverse(binary)]
program: String,
arg: String,
}
};
let generator = parse_ok(input);
assert!(generator.struct_attributes.binary.is_none());
assert!(generator.fields_attributes.iter().any(|f| f.binary));
}
#[test]
fn reject_struct_with_both_static_and_dynamic_binary() {
let input: DeriveInput = parse_quote! {
#[clap_reverse(binary = "java")]
struct Test {
#[clap_reverse(binary)]
program: String,
}
};
let err = ClapReverseGenerator::parse(input).unwrap_err();
assert!(err.to_string().contains("binary"));
}
#[test]
fn reject_struct_with_multiple_dynamic_binaries() {
let input: DeriveInput = parse_quote! {
struct Test {
#[clap_reverse(binary)]
program1: String,
#[clap_reverse(binary)]
program2: String,
}
};
let err = ClapReverseGenerator::parse(input).unwrap_err();
assert!(err.to_string().contains("ONLY on ONE"));
}
#[test]
fn generate_as_command_with_static_binary() {
let input: DeriveInput = parse_quote! {
#[clap_reverse(binary = "echo")]
struct Test {
#[clap_reverse(flag)]
verbose: bool,
arg: String,
}
};
let generator = parse_ok(input);
let tokens = generator.generate().unwrap();
let code = tokens.to_string();
assert!(code.contains("impl clap_reverse :: AsCommand for Test"));
assert!(code.contains("echo"));
}
#[test]
fn generate_display_impl_when_enabled() {
let input: DeriveInput = parse_quote! {
#[clap_reverse(binary = "echo", display)]
struct Test {
arg: String,
}
};
let generator = parse_ok(input);
let tokens = generator.generate().unwrap();
let code = tokens.to_string();
assert!(code.contains("impl std :: fmt :: Display for Test"));
}
#[test]
fn skip_display_impl_when_not_enabled() {
let input: DeriveInput = parse_quote! {
#[clap_reverse(binary = "echo")]
struct Test {
arg: String,
}
};
let generator = parse_ok(input);
let tokens = generator.generate().unwrap();
let code = tokens.to_string();
assert!(!code.contains("Display"));
}
}