clap_reverse_impl 0.1.1

Internal subcrate for `clap_reverse` crate
Documentation
//! Core generator for the `ClapReverse` derive macro.
//!
//! This module provides the [`ClapReverseGenerator`] which is responsible for:
//! - Parsing [`StructAttributes`] and [`FieldAttributes`] from the input AST.
//! - Validating that binary configuration is consistent.
//! - Generating implementations for [`clap_reverse::AsCommand`] and optionally [`std::fmt::Display`].

use crate::{FieldAttributes, StructAttributes};


/// Generator for the `ClapReverse` derive macro.
///
/// This struct encapsulates the parsed input (`syn::DeriveInput`),
/// struct-level attributes ([`StructAttributes`]), and field-level attributes ([`FieldAttributes`]).
/// It validates them and produces trait implementations.
pub struct ClapReverseGenerator {
    /// The input syntax tree [`DeriveInput`] for the struct the macro is applied to.
    input: syn::DeriveInput,
    /// Attributes applied at the struct level (e.g. `prefix`, `delimiter`, `binary`, `display`).
    struct_attributes: StructAttributes,
    /// Attributes applied to each field of the struct (e.g. `flag`, `transparent`, `prefix`).
    fields_attributes: Vec<FieldAttributes>,
}

impl ClapReverseGenerator {
    /// Parses a [`syn::DeriveInput`] into a [`ClapReverseGenerator`].
    ///
    /// - Validates that the input is a struct (not enum/union).
    /// - Parses struct-level attributes via [`StructAttributes::parse`].
    /// - Parses each field’s attributes via [`FieldAttributes::parse`].
    /// - Ensures **exactly one `binary`** is set, either statically on the struct or dynamically on one field, but not both.
    /// - Rejects multiple dynamic `binary` fields.
    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,
        })
    }

    /// Generates all trait implementations for the target struct.
    ///
    /// Returns a [`proc_macro2::TokenStream`] containing:
    /// - An [`clap_reverse::AsCommand`] implementation (always).
    /// - A [`std::fmt::Display`] implementation if `display` attribute is enabled.
    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
        })
    }

    /// Generates the [`clap_reverse::AsCommand`] implementation for the struct.
    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
                }
            }
        }
    }

    /// Resolves the binary to use for the command.
    ///
    /// - If `binary` is set on the struct, returns it as a string literal.
    /// - If `binary` is set on one field, references the field value at runtime.
    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.");
        }
    }

    /// Builds the command arguments as tokens for inclusion in the [`AsCommand`] impl.
    ///
    /// For each field:
    /// - **flag**: Adds `--{name}` if the boolean is true.
    /// - **transparent**: Adds the raw field value as-is.
    /// - **Option<T>**: Adds the argument only if `Some`.
    /// - **Other types**: Always adds `--{name}={value}`.
    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<_>>()
    }

    /// Optionally generates a [`std::fmt::Display`] implementation.
    ///
    /// When enabled (`display = true`),
    /// prints the reconstructed command line as a single string by joining the program name and arguments.
    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"));
    }
}