plural_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, Attribute, DeriveInput, Lit};
4
5/// Procedural macro to derive `PluralDisplay`.
6#[proc_macro_derive(Plural, attributes(plural))]
7pub fn derive_plural(input: TokenStream) -> TokenStream {
8    // Parse the input enum
9    let input = parse_macro_input!(input as DeriveInput);
10
11    // Ensure it's an enum
12    let ident = input.ident.clone();
13    let data = if let syn::Data::Enum(data) = input.data {
14        data
15    } else {
16        return syn::Error::new_spanned(input, "Plural can only be derived for enums")
17            .to_compile_error()
18            .into();
19    };
20
21    if data.variants.is_empty() {
22        // Handle empty enums by generating a dummy implementation
23        return quote! {
24            impl PluralDisplay for #ident {
25                fn plural(&self) -> &'static str {
26                    unreachable!("Empty enums cannot have instances.");
27                }
28            }
29        }
30        .into();
31    }
32
33    // Generate plural forms for each variant
34    let variants = data.variants.iter().map(|variant| {
35        let variant_ident = &variant.ident;
36
37        // Check for #[plural("...")] attribute
38        let custom_plural = find_custom_plural(&variant.attrs);
39
40        // Use custom plural if provided, else generate default
41        let plural_form = if let Some(custom) = custom_plural {
42            custom
43        } else {
44            // Convert CamelCase to lowercase with spaces and append 's'
45            let name = variant_ident.to_string();
46            let spaced = name
47                .chars()
48                .flat_map(|c| if c.is_uppercase() {
49                    vec![' ', c.to_ascii_lowercase()]
50                } else {
51                    vec![c]
52                })
53                .collect::<String>()
54                .trim()
55                .to_owned();
56            format!("{}s", spaced)
57        };
58
59        // Generate match arm
60        quote! {
61            Self::#variant_ident => #plural_form,
62        }
63    });
64
65    // Implement the PluralDisplay trait
66    let expanded = quote! {
67        impl PluralDisplay for #ident {
68            fn plural(&self) -> &'static str {
69                match self {
70                    #(#variants)*
71                }
72            }
73        }
74    };
75
76    expanded.into()
77}
78
79/// Extracts the custom plural value from the `#[plural("...")]` attribute, if present.
80fn find_custom_plural(attrs: &[Attribute]) -> Option<String> {
81    for attr in attrs {
82        if attr.path().is_ident("plural") {
83            if let Some(Lit::Str(lit_str)) = attr.parse_args::<Lit>().ok() {
84                return Some(lit_str.value());
85            }
86        }
87    }
88    None
89}