matched_enums_macro 1.3.0

Contains the macro for matchable enums.
Documentation
use super::MatchAttribute;

use hashbrown::HashMap;
use syn::{Ident, Meta, Type, parse2, spanned::Spanned};

#[derive(Debug, Clone)]
pub struct Variant {
    pub ident: Ident,
    /// If no specific matcher can be found, this should be used as a fallback.
    /// However, this default is optional when [`Self::matchers`] covers all bases.
    default_matcher: Option<MatchAttribute>,
    matchers: HashMap<Type, MatchAttribute>,
}

impl Variant {
    pub fn get_match_attr_for(&self, type_bind: &Type) -> Option<&MatchAttribute> {
        self.matchers.get(type_bind)
    }

    /// Attempts [`Self::get_match_attr_for`], if the type was not found, this will attempt to return the default value.
    pub fn get_match_attr_for_or_default(&self, type_bind: &Type) -> Option<&MatchAttribute> {
        self.get_match_attr_for(type_bind)
            .or(self.default_matcher.as_ref())
    }

    pub fn get_precedence(&self, type_bind: &Type) -> Option<u16> {
        self.get_match_attr_for_or_default(type_bind)
            .map(|attr| attr.precedence)
    }
}

impl TryFrom<syn::Variant> for Variant {
    type Error = syn::Error;

    fn try_from(value: syn::Variant) -> syn::Result<Self> {
        const ATTR_NAME_MATCHER: &str = "matches";

        let mut variant = Self {
            ident: value.ident.clone(),
            default_matcher: None,
            matchers: HashMap::new(),
        };

        for attr in value
            .attrs
            .iter()
            .filter(|attr| attr.path().is_ident(ATTR_NAME_MATCHER))
        {
            let attr = match &attr.meta {
                Meta::List(meta) => parse2::<MatchAttribute>(meta.tokens.clone())?,

                _ => {
                    return Err(syn::Error::new(
                        attr.span(),
                        format_args!(
                            "`{ATTR_NAME_MATCHER}` should be assigned through as a list (e.g., `#[{ATTR_NAME_MATCHER}(0..42)]`)."
                        ),
                    ));
                }
            };

            match &attr.type_bind {
                Some(bind) => {
                    if variant
                        .matchers
                        .insert(bind.clone(), attr.clone())
                        .is_some()
                    {
                        return Err(syn::Error::new(
                            bind.span(),
                            format_args!("Multiple matches found for type {bind:?}"),
                        ));
                    }
                }
                None => {
                    if variant.default_matcher.is_some() {
                        return Err(syn::Error::new(
                            attr.span(),
                            "Only one default match can be specified per type",
                        ));
                    }

                    variant.default_matcher = Some(attr)
                }
            }
        }

        Ok(variant)
    }
}

#[cfg(test)]
mod test;