polybox-codegen 0.0.2

Message-passing abstractions to make working with channels and actors a more seamless experience
Documentation
use heck::ToSnakeCase;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{
    parse2, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Colon, Error, Fields,
    ItemStruct, Meta, Pat, PatType, Token,
};

pub fn derive_envelope(item: TokenStream) -> Result<TokenStream, Error> {
    let item = parse2::<ItemStruct>(item)?;

    if !item.generics.params.is_empty() {
        Err(Error::new_spanned(
            item.generics.params,
            "Generics currently not supported with Envelope",
        ))?
    }

    let (trait_name, method_name) = match item
        .attrs
        .iter()
        .find(|attr| attr.path.is_ident("envelope"))
    {
        Some(attr) => {
            let Meta::List(meta_list) = attr.parse_meta()? else {
                panic!()
            };
            let mut iter = meta_list.nested.into_iter();
            let Some(trait_name) = iter.next() else {
                Err(Error::new_spanned(&meta_list.path, "Expected trait name"))?
            };
            let Some(fn_name) = iter.next() else {
                Err(Error::new_spanned(&meta_list.path, "Expected method name"))?
            };
            (
                parse2(trait_name.to_token_stream())?,
                parse2(fn_name.to_token_stream())?,
            )
        }
        None => {
            let trait_ident = Ident::new(
                &format!("{}Envelope", item.ident.to_string()),
                item.ident.span(),
            );
            let method_ident =
                Ident::new(&item.ident.to_string().to_snake_case(), item.ident.span());
            (trait_ident, method_ident)
        }
    };
    let trait_doc = format!(
        "
        Automatically generated trait for creating envelopes of the
        message[`{}`].
        ",
        item.ident
    );
    let method_doc = format!(
        "
        Creates an [`Envelope`](::polybox::messaging::Envelope) for the
        message[`{}`].
        ",
        item.ident
    );
    let ident = item.ident;
    let vis = item.vis;
    let (field_params, field_idents) = parse_fields(item.fields);

    Ok(quote! {
        #[doc = #trait_doc]
        #vis trait #trait_name: ::polybox::actor_reference::ActorRef
        where
            Self::ActorType: ::polybox::messaging::Accepts<#ident>
        {
            #[doc = #method_doc]
            fn #method_name(
                &self,
                #field_params
            ) -> ::polybox::messaging::Envelope<'_, Self::ActorType, #ident> {
                <Self as ::polybox::actor_reference::ActorRefExt>::envelope(self, #ident {
                    #field_idents
                })
            }
        }

        impl<T> #trait_name for T
        where
            T: ::polybox::actor_reference::ActorRef,
            T::ActorType: ::polybox::messaging::Accepts<#ident>
        { }
    })
}

pub fn parse_fields(
    fields: Fields,
) -> (
    Punctuated<PatType, Token![,]>,
    Punctuated<Box<Pat>, Token![,]>,
) {
    let params = match fields {
        Fields::Named(named_fields) => named_fields
            .named
            .into_iter()
            .enumerate()
            .map(|(i, field)| {
                let span = field.span();
                let ident = field.ident.unwrap_or(Ident::new(&format!("arg{i}"), span));
                PatType {
                    attrs: field.attrs,
                    pat: parse_quote!(#ident),
                    colon_token: Colon(Span::call_site()),
                    ty: field.ty.into(),
                }
            })
            .collect::<Punctuated<_, Token![,]>>(),
        Fields::Unnamed(_) => todo!(),
        Fields::Unit => todo!(),
    };
    let fields = params
        .iter()
        .map(|field| field.pat.clone())
        .collect::<Punctuated<_, Token![,]>>();
    (params, fields)
}