mplusfonts-macros 0.3.4

Procedural macros re-exported in the mplusfonts crate
Documentation
use super::ExprPathExt;

const CONSTANTS: [(&str, u16); 2] = [("NORMAL", 100), ("EXPANDED", 125)];

pub struct FontWidth<const MAX: u16>(pub u16);

impl<const MAX: u16> TryFrom<syn::Expr> for FontWidth<MAX> {
    type Error = syn::Error;

    fn try_from(expr: syn::Expr) -> Result<Self, Self::Error> {
        let width = match expr {
            syn::Expr::Lit(expr_lit) => expr_lit.try_into()?,
            syn::Expr::Path(expr_path) => expr_path.try_into()?,
            expr => {
                let message = "expected literal or identifier";
                return Err(syn::Error::new_spanned(expr, message));
            }
        };

        Ok(width)
    }
}

impl<const MAX: u16> TryFrom<syn::ExprLit> for FontWidth<MAX> {
    type Error = syn::Error;

    fn try_from(expr_lit: syn::ExprLit) -> Result<Self, Self::Error> {
        let syn::Lit::Int(lit_int) = expr_lit.lit else {
            let message = "expected integer literal";
            return Err(syn::Error::new_spanned(expr_lit.lit, message));
        };

        let value = lit_int.base10_parse()?;
        if value < 100 || value > MAX {
            let message = format!("expected number between `100` and `{MAX}`, found `{value}`");
            return Err(syn::Error::new(lit_int.span(), message));
        }

        Ok(Self(value))
    }
}

impl<const MAX: u16> TryFrom<syn::ExprPath> for FontWidth<MAX> {
    type Error = syn::Error;

    fn try_from(expr_path: syn::ExprPath) -> Result<Self, Self::Error> {
        let ident = expr_path.try_into_ident()?;
        let name = ident.to_string();
        let mut options = Vec::new();
        for (const_name, value) in CONSTANTS {
            if value < 100 || value > MAX {
                continue;
            }

            if name == const_name {
                return Ok(Self(value));
            }

            options.push(const_name);
        }

        let [first, rest @ ..] = options.as_slice() else {
            panic!("expected `MAX` greater than or equal to `100`, found `{MAX}`");
        };
        let message = if rest.is_empty() {
            format!("expected `{first}`, found `{name}`")
        } else if let [second] = rest {
            format!("expected `{first}` or `{second}`, found `{name}`")
        } else {
            let rest = rest.join("`, `");
            format!("expected `{first}` or one of: `{rest}`; found `{name}`")
        };
        let error = syn::Error::new(ident.span(), message);

        Err(error)
    }
}