use std::collections::{BTreeMap, BTreeSet}; use proc_macro2::Span; use quote::quote; use syn::{Attribute, Ident, Path}; pub(crate) fn expand<'a>( prefix_ident: &Ident, variants: impl Iterator)>, ) -> proc_macro2::TokenStream { // Build up a map from prefix to matching RuleCodes. let mut prefix_to_codes: BTreeMap> = BTreeMap::default(); let mut code_to_attributes: BTreeMap = BTreeMap::default(); for (variant, group, attr) in variants { let code_str = variant.to_string(); // Nursery rules have to be explicitly selected, so we ignore them when looking at prefixes. if is_nursery(group) { prefix_to_codes .entry(code_str.clone()) .or_default() .insert(code_str.clone()); } else { for i in 1..=code_str.len() { let prefix = code_str[..i].to_string(); prefix_to_codes .entry(prefix) .or_default() .insert(code_str.clone()); } } code_to_attributes.insert(code_str, attr); } let variant_strs: Vec<_> = prefix_to_codes.keys().collect(); let variant_idents: Vec<_> = prefix_to_codes .keys() .map(|prefix| { let ident = get_prefix_ident(prefix); quote! { #ident } }) .collect(); let attributes: Vec<_> = prefix_to_codes .values() .map(|codes| attributes_for_prefix(codes, &code_to_attributes)) .collect(); quote! { #[derive( ::strum_macros::EnumIter, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, )] pub enum #prefix_ident { #(#attributes #variant_idents,)* } impl std::str::FromStr for #prefix_ident { type Err = crate::registry::FromCodeError; fn from_str(code: &str) -> Result { match code { #(#attributes #variant_strs => Ok(Self::#variant_idents),)* _ => Err(crate::registry::FromCodeError::Unknown) } } } impl From<&#prefix_ident> for &'static str { fn from(code: &#prefix_ident) -> Self { match code { #(#attributes #prefix_ident::#variant_idents => #variant_strs,)* } } } impl AsRef for #prefix_ident { fn as_ref(&self) -> &str { match self { #(#attributes Self::#variant_idents => #variant_strs,)* } } } } } fn attributes_for_prefix( codes: &BTreeSet, attributes: &BTreeMap, ) -> proc_macro2::TokenStream { match if_all_same(codes.iter().map(|code| attributes[code])) { Some(attr) => quote!(#(#attr)*), None => quote!(), } } /// If all values in an iterator are the same, return that value. Otherwise, /// return `None`. pub(crate) fn if_all_same(iter: impl Iterator) -> Option { let mut iter = iter.peekable(); let first = iter.next()?; if iter.all(|x| x == first) { Some(first) } else { None } } /// Returns an identifier for the given prefix. pub(crate) fn get_prefix_ident(prefix: &str) -> Ident { let prefix = if prefix.as_bytes()[0].is_ascii_digit() { // Identifiers in Rust may not start with a number. format!("_{prefix}") } else { prefix.to_string() }; Ident::new(&prefix, Span::call_site()) } /// Returns true if the given group is the "nursery" group. pub(crate) fn is_nursery(group: &Path) -> bool { let group = group .segments .iter() .map(|segment| segment.ident.to_string()) .collect::>() .join("::"); group == "RuleGroup::Nursery" }