vexide-macro 0.4.0

Procedural macros for vexide.
Documentation
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{
    Ident, LitBool, Result, Token, parenthesized,
    parse::{Parse, ParseStream},
    punctuated::Punctuated,
    token,
};

mod kw {
    use syn::custom_keyword;

    custom_keyword!(banner);

    custom_keyword!(enabled);
    custom_keyword!(theme);

    custom_keyword!(code_sig);
}

#[derive(Clone)]
pub struct MacroOpts {
    pub banner_enabled: bool,
    pub banner_theme: Option<Ident>,
    pub code_sig: Option<Ident>,
}

impl Default for MacroOpts {
    fn default() -> Self {
        Self {
            banner_enabled: true,
            banner_theme: None,
            code_sig: None,
        }
    }
}

impl From<Attrs> for MacroOpts {
    fn from(value: Attrs) -> Self {
        let mut opts = Self::default();
        for attr in value.attr_list {
            match attr {
                Attribute::Banner(banner) => {
                    for attr in banner.attrs {
                        match attr {
                            BannerAttribute::Enabled(enabled) => {
                                opts.banner_enabled = enabled.as_bool();
                            }
                            BannerAttribute::Theme(theme) => {
                                opts.banner_theme = Some(theme.into_ident());
                            }
                        }
                    }
                }
                Attribute::CodeSig(code_sig) => opts.code_sig = Some(code_sig.into_ident()),
            }
        }
        opts
    }
}

pub struct Attrs {
    attr_list: Punctuated<Attribute, Token![,]>,
}

impl Parse for Attrs {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        Ok(Self {
            attr_list: Punctuated::parse_terminated(input)?,
        })
    }
}

pub enum Attribute {
    Banner(Banner),
    CodeSig(CodeSig),
}

impl Parse for Attribute {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(kw::banner) {
            input.parse().map(Attribute::Banner)
        } else if lookahead.peek(kw::code_sig) {
            input.parse().map(Attribute::CodeSig)
        } else {
            Err(lookahead.error())
        }
    }
}

pub struct Banner {
    token: kw::banner,
    paren: token::Paren,
    attrs: Punctuated<BannerAttribute, Token![,]>,
}

impl Parse for Banner {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let attrs;
        Ok(Self {
            token: input.parse()?,
            paren: parenthesized!(attrs in input),
            attrs: attrs.parse_terminated(BannerAttribute::parse, Token![,])?,
        })
    }
}

impl ToTokens for Banner {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        self.token.to_tokens(tokens);
        self.paren.surround(tokens, |tokens| {
            self.attrs.to_tokens(tokens);
        });
    }
}

pub enum BannerAttribute {
    Enabled(BannerEnabled),
    Theme(BannerTheme),
}
impl Parse for BannerAttribute {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(kw::enabled) {
            input.parse().map(BannerAttribute::Enabled)
        } else if lookahead.peek(kw::theme) {
            input.parse().map(BannerAttribute::Theme)
        } else {
            Err(lookahead.error())
        }
    }
}

impl ToTokens for BannerAttribute {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        match self {
            BannerAttribute::Enabled(enabled) => enabled.to_tokens(tokens),
            BannerAttribute::Theme(theme) => theme.to_tokens(tokens),
        }
    }
}

pub struct BannerEnabled {
    token: kw::enabled,
    eq: Token![=],
    value: LitBool,
}
impl Parse for BannerEnabled {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        Ok(Self {
            token: input.parse()?,
            eq: input.parse()?,
            value: input.parse()?,
        })
    }
}

impl BannerEnabled {
    pub const fn as_bool(&self) -> bool {
        self.value.value
    }
}

impl ToTokens for BannerEnabled {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        self.token.to_tokens(tokens);
        self.eq.to_tokens(tokens);
        self.value.to_tokens(tokens);
    }
}

pub struct BannerTheme {
    token: kw::theme,
    eq: Token![=],
    ident: Ident,
}

impl Parse for BannerTheme {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        Ok(Self {
            token: input.parse()?,
            eq: input.parse()?,
            ident: input.parse()?,
        })
    }
}

impl BannerTheme {
    pub fn into_ident(self) -> Ident {
        self.ident
    }
}

impl ToTokens for BannerTheme {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        self.token.to_tokens(tokens);
        self.eq.to_tokens(tokens);
        self.ident.to_tokens(tokens);
    }
}

pub struct CodeSig {
    token: kw::code_sig,
    eq: Token![=],
    ident: Ident,
}

impl CodeSig {
    pub fn into_ident(self) -> Ident {
        self.ident
    }
}

impl Parse for CodeSig {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        Ok(Self {
            token: input.parse()?,
            eq: input.parse()?,
            ident: input.parse()?,
        })
    }
}

impl ToTokens for CodeSig {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        self.token.to_tokens(tokens);
        self.eq.to_tokens(tokens);
        self.ident.to_tokens(tokens);
    }
}

#[cfg(test)]
mod test {
    use quote::quote;
    use syn::Ident;

    use super::*;

    #[test]
    fn parses_banner_attribute() {
        let source = quote! {
            banner(enabled = true, theme = THEME)
        };
        let input = syn::parse2::<Banner>(source).unwrap();
        assert!(input.attrs.len() == 2);
        assert!(matches!(
            input.attrs[0],
            BannerAttribute::Enabled(BannerEnabled {
                value: LitBool {
                    value: true,
                    span: _
                },
                ..
            })
        ));
        assert!(matches!(input.attrs[1], BannerAttribute::Theme(_)));
    }

    #[test]
    fn parses_code_sig_attribute() {
        let ident = Ident::new("my_code_sig", proc_macro2::Span::call_site());
        let source = quote! {
            code_sig = #ident
        };
        let input = syn::parse2::<CodeSig>(source).unwrap();
        assert_eq!(input.into_ident(), ident);
    }

    #[test]
    fn parses_attrs_into_macro_opts() {
        let source = quote! {
            banner(enabled = true, theme = THEME), code_sig = my_code_sig
        };
        let input = syn::parse2::<Attrs>(source).unwrap();
        assert_eq!(input.attr_list.len(), 2);
        let opts = MacroOpts::from(input);
        assert!(opts.banner_enabled);
        assert_eq!(opts.code_sig.unwrap().to_string(), "my_code_sig");
    }

    #[test]
    fn macro_opts_defaults_when_n_opts_missing() {
        fn macro_opts_from(source: TokenStream) -> MacroOpts {
            let input = syn::parse2::<Attrs>(source).unwrap();
            MacroOpts::from(input)
        }

        let source = quote! {};
        let opts = macro_opts_from(source);
        assert!(opts.banner_enabled);
        assert_eq!(opts.code_sig, None);

        let source = quote! {
            banner(enabled = false)
        };
        let opts = macro_opts_from(source);
        assert!(!opts.banner_enabled);
        assert_eq!(opts.code_sig, None);

        let source = quote! {
            code_sig = my_code_sig
        };
        let opts = macro_opts_from(source);
        assert!(opts.banner_enabled);
        assert_eq!(opts.code_sig.unwrap().to_string(), "my_code_sig");

        let source = quote! {
            banner(enabled = false), code_sig = my_code_sig
        };
        let opts = macro_opts_from(source);
        assert!(!opts.banner_enabled);
        assert_eq!(opts.code_sig.unwrap().to_string(), "my_code_sig");
    }
}