cranpose-macros 0.1.171

Procedural macros for Cranpose
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Ident, ItemFn, LitBool, LitInt, LitStr, ReturnType};

struct Options {
    name: LitStr,
    group: LitStr,
    width: u32,
    height: u32,
    dark: bool,
}

impl Options {
    fn parse(args: TokenStream, ident: &Ident) -> syn::Result<Self> {
        let mut options = Self {
            name: LitStr::new(&ident.to_string(), ident.span()),
            group: LitStr::new("", ident.span()),
            width: 480,
            height: 640,
            dark: false,
        };
        let parser = syn::meta::parser(|meta| options.parse_option(meta));
        syn::parse::Parser::parse2(parser, args)?;
        options.validate_dimensions(ident)?;
        Ok(options)
    }

    fn parse_option(&mut self, meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
        if meta.path.is_ident("name") {
            self.name = meta.value()?.parse::<LitStr>()?;
        } else if meta.path.is_ident("group") {
            self.group = meta.value()?.parse::<LitStr>()?;
        } else if meta.path.is_ident("width") {
            self.width = meta.value()?.parse::<LitInt>()?.base10_parse()?;
        } else if meta.path.is_ident("height") {
            self.height = meta.value()?.parse::<LitInt>()?.base10_parse()?;
        } else if meta.path.is_ident("dark") {
            self.dark = meta.value()?.parse::<LitBool>()?.value;
        } else {
            return Err(meta.error("supported preview options: name, group, width, height, dark"));
        }
        Ok(())
    }

    fn validate_dimensions(&self, ident: &Ident) -> syn::Result<()> {
        if !(1..=8192).contains(&self.width) || !(1..=8192).contains(&self.height) {
            return Err(syn::Error::new_spanned(
                ident,
                "preview dimensions must be between 1 and 8192",
            ));
        }
        Ok(())
    }
}

fn validate(function: &ItemFn) -> syn::Result<()> {
    if !function.sig.inputs.is_empty()
        || !function.sig.generics.params.is_empty()
        || function.sig.asyncness.is_some()
    {
        return Err(syn::Error::new_spanned(
            &function.sig,
            "preview functions must be synchronous, have no parameters, and have no generics",
        ));
    }
    if !matches!(&function.sig.output, ReturnType::Default)
        && !matches!(&function.sig.output, ReturnType::Type(_, ty) if matches!(&**ty, syn::Type::Tuple(tuple) if tuple.elems.is_empty()))
    {
        return Err(syn::Error::new_spanned(
            &function.sig.output,
            "preview functions must return ()",
        ));
    }
    Ok(())
}

pub(crate) fn expand(args: TokenStream, function: ItemFn) -> syn::Result<TokenStream> {
    validate(&function)?;
    let ident = &function.sig.ident;
    let Options {
        name,
        group,
        width,
        height,
        dark,
    } = Options::parse(args, ident)?;
    let path = crate::core_crate_path();
    Ok(quote! {
        #function
        #path::preview::__submit! {
            #path::preview::Preview {
                id: concat!(module_path!(), "::", stringify!(#ident), "@", #name, "/", stringify!(#width), "x", stringify!(#height), "/", stringify!(#dark)),
                name: #name, group: #group, function: stringify!(#ident),
                file: file!(), line: line!(), width: #width, height: #height, dark: #dark,
                render: #ident,
            }
        }
    })
}