clap_reverse_impl 0.1.1

Internal subcrate for `clap_reverse` crate
Documentation
//! Struct-level attribute parsing and validation.
//!
//! This module defines [`StructAttributes`], which stores metadata parsed from `#[clap_reverse(...)]` attributes applied at the struct level.
//! These attributes define defaults and global behaviors for the generated command.

use quote::ToTokens;
use crate::move_it_somewhere::find_reverse_clap_attribute;


/// Parsed attributes for a struct.
///
/// Represents configuration that applies to the entire command.
/// Each attribute can be set via `#[clap_reverse(...)]` on the struct.
#[derive(Debug, PartialEq, Eq)]
pub struct StructAttributes {
    /// Whether to generate a `Display` implementation that renders the command string.
    pub display: bool,
    /// Default prefix for arguments (e.g. `"--"`).
    /// Can be overridden per-field with `#[clap_reverse(prefix = "...")]`.
    pub prefix: String,
    /// Default delimiter between argument name and value (e.g. `" "` or `"="`).
    /// Can be overridden per-field with `#[clap_reverse(delimiter = "...")]`.
    pub delimiter: String,
    /// Fixed binary name or path for the command.
    /// If not set, a field may provide the binary instead.
    pub binary: Option<String>,
}

impl Default for StructAttributes {
    fn default() -> Self {
        Self {
            display: false,
            prefix: "--".to_string(),
            delimiter: " ".to_string(),
            binary: None,
        }
    }
}

impl StructAttributes {
    /// Documentation for all supported struct-level attributes.
    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\"`"),
    ];

    /// Parse struct-level attributes from a [`syn::DeriveInput`].
    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)
    }

    /// Parse a single struct-level attribute and update `self`.
    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())))
        }
    }

    /// Format an error message for an unknown attribute.
    ///
    /// Lists all supported struct-level attributes with usage hints.
    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"));
    }
}