vorant_gen 0.2.0

A proc-macro library for generating state machines
Documentation
use proc_macro2::Span;
use quote::quote;
use syn::{
    Ident, Token, Type,
    parse::{Parse, Parser},
    punctuated::Punctuated,
};

pub struct PointSave {
    pub items: Punctuated<PointSaveItem, Token![,]>,
}

impl PointSave {
    pub fn can_from(attrs: &[syn::Attribute]) -> bool {
        attrs.iter().any(|attr| {
            matches!(&attr.meta,
            syn::Meta::List(syn::MetaList {
                path,
                ..
            }) if is_save_path(path)
            )
        })
    }

    pub const fn expand_def(&self) -> ExpandDef<'_> {
        ExpandDef(self)
    }

    pub const fn expand_destructure(&self) -> ExpandDestructure<'_> {
        ExpandDestructure(self)
    }

    pub const fn expand_constructor(&self) -> ExpandConstructor<'_> {
        ExpandConstructor(self)
    }
}

pub struct ExpandDef<'p>(&'p PointSave);

impl<'p> quote::ToTokens for ExpandDef<'p> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let fields = self.0.items.iter().map(|item| {
            let ident = &item.ident;
            let ty = &item.ty;

            quote! {
                #ident: #ty
            }
        });

        tokens.extend(quote! { #(#fields),* });
    }
}

pub struct ExpandDestructure<'p>(&'p PointSave);

impl<'p> quote::ToTokens for ExpandDestructure<'p> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let fields = self.0.items.iter().map(|item| {
            let ident = &item.ident;
            let mutability = &item.mutability;

            quote! {
                #mutability #ident
            }
        });

        tokens.extend(quote! { #(#fields),* });
    }
}

pub struct ExpandConstructor<'p>(&'p PointSave);

impl<'p> quote::ToTokens for ExpandConstructor<'p> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let fields = self.0.items.iter().map(|item| &item.ident);

        tokens.extend(quote! { #(#fields),* });
    }
}

pub struct PointSaveItem {
    pub mutability: Option<Token![mut]>,
    pub ident: Ident,
    pub _colon_token: Token![:],
    pub ty: Type,
}

impl Parse for PointSaveItem {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let mut_token = if input.peek(Token![mut]) {
            Some(input.parse::<Token![mut]>()?)
        } else {
            None
        };

        let ident = input.parse::<Ident>()?;
        let colon_token = input.parse::<Token![:]>()?;
        let ty = input.parse::<Type>()?;

        Ok(PointSaveItem {
            mutability: mut_token,
            ident,
            _colon_token: colon_token,
            ty,
        })
    }
}

impl TryFrom<(Span, Vec<syn::Attribute>)> for PointSave {
    type Error = syn::Error;

    fn try_from((span, attrs): (Span, Vec<syn::Attribute>)) -> Result<Self, Self::Error> {
        let tokens = attrs.into_iter().find_map(|attr| match attr.meta {
            syn::Meta::List(syn::MetaList {
                path,
                delimiter: syn::MacroDelimiter::Brace(_),
                tokens,
            }) if is_save_path(&path) => Some(tokens),
            _ => None,
        });

        let Some(tokens) = tokens else {
            return Err(syn::Error::new(
                span,
                "expected `vorant::save { .. }` attribute",
            ));
        };

        let items = Punctuated::<PointSaveItem, Token![,]>::parse_terminated.parse2(tokens)?;

        Ok(PointSave { items })
    }
}

fn is_save_path(path: &syn::Path) -> bool {
    path.leading_colon.is_none()
        && path.segments.len() == 2
        && path.segments[0].ident == "vorant"
        && path.segments[1].ident == "save"
}

#[cfg(test)]
mod tests {
    use super::*;
    use syn::parse_quote;

    #[test]
    fn checks_save_path() {
        let path: syn::Path = parse_quote!(vorant::save);
        assert!(is_save_path(&path), "expected `vorant::save`");
    }
}