servify_macro 0.1.0

Procedural macros for use with Servify
Documentation
use case::CaseExt;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
    parse::{ParseStream, Parser},
    punctuated::Punctuated,
    spanned::Spanned,
    Error, Expr, ExprField, ExprPath, Field, FieldMutability, FieldValue, FieldsNamed, FnArg,
    Ident, ImplItem, ImplItemFn, ItemImpl, Member, Pat, PatType, Result, Token, TypePath,
    Visibility,
};

use crate::util::{return_type_ext::ReturnTypeExt, type_path_ext::TypePathExt};

pub(crate) fn impl_export(_attrs: TokenStream, item: TokenStream) -> TokenStream {
    parse.parse2(item).unwrap_or_else(Error::into_compile_error)
}

struct ExportParent {
    mod_path: TypePath,
}

fn parse(input: ParseStream) -> Result<TokenStream> {
    let top: ItemImpl = input.parse()?;
    let mod_path = match *top.self_ty {
        syn::Type::Path(path) => path,
        _ => Err(Error::new(
            top.self_ty.span(),
            "servify_macro::export can only be used on impl blocks with a TypePath.",
        ))?,
    };

    let parent = ExportParent { mod_path };
    top.items
        .iter()
        .map(|item| match item {
            ImplItem::Fn(item) => parse_method(item, &parent),
            item => Err(Error::new(
                item.span(),
                "servify_macro::export cannot handle implementations other than functions.",
            )),
        })
        .collect()
}

fn parse_method(input: &ImplItemFn, parent: &ExportParent) -> Result<TokenStream> {
    let mod_path = parent.mod_path.clone();

    let struct_name = mod_path.path.segments.last().unwrap().ident.clone();

    let fn_name = input.sig.ident.clone();

    let export_name = Ident::new(&format!("{}_{}", struct_name, fn_name), Span::call_site());

    let request_name = Ident::new(
        &format!("__{}_request", fn_name.to_string().to_snake()),
        Span::call_site(),
    );

    let response_name = Ident::new(
        &format!("__{}_response", fn_name.to_string().to_snake()),
        Span::call_site(),
    );

    let server_path = mod_path.clone().with_trail_ident("Server");
    let client_path = mod_path.clone().with_trail_ident("Client");

    let internal_fn_name = Ident::new(&format!("__internal_{}", fn_name), Span::call_site());

    let sig = input.sig.inputs.clone();

    let sig_without_self = sig
        .clone()
        .into_iter()
        .filter(|i| match i {
            FnArg::Typed(_) => true,
            FnArg::Receiver(_) => false,
        })
        .collect::<Punctuated<FnArg, Token![,]>>();

    let body = input.block.clone();
    let response = input.sig.output.clone().to_type();

    let request_sig = sig_without_self
        .clone()
        .into_iter()
        .filter_map(|i| match i {
            FnArg::Typed(PatType { pat, ty, .. }) => match *pat {
                Pat::Ident(ident) => {
                    Some((Ident::new(&ident.ident.to_string(), Span::call_site()), *ty))
                }
                _ => None,
            },
            _ => None,
        })
        .collect::<Vec<_>>();

    let struct_block = FieldsNamed {
        brace_token: Default::default(),
        named: request_sig
            .clone()
            .into_iter()
            .map(|(ident, ty)| Field {
                attrs: Default::default(),
                vis: Visibility::Inherited,
                ident: Some(ident),
                colon_token: Default::default(),
                ty,
                mutability: FieldMutability::None,
            })
            .collect(),
    };

    let call_server_args: Punctuated<ExprField, Token![,]> = request_sig
        .clone()
        .into_iter()
        .map(|(ident, _)| ExprField {
            attrs: Default::default(),
            member: Member::Named(Ident::new(&ident.to_string(), Span::call_site())),
            dot_token: Default::default(),
            base: Box::new(Expr::Path(ExprPath {
                attrs: Default::default(),
                qself: None,
                path: Ident::new("req", Span::call_site()).into(),
            })),
        })
        .collect();

    let call_client_args: Punctuated<FieldValue, Token![,]> = request_sig
        .clone()
        .into_iter()
        .map(|(ident, _)| FieldValue {
            attrs: Default::default(),
            member: Member::Named(Ident::new(&ident.to_string(), Span::call_site())),
            colon_token: Default::default(),
            expr: Expr::Path(ExprPath {
                attrs: Default::default(),
                qself: None,
                path: Ident::new(&ident.to_string(), Span::call_site()).into(),
            }),
        })
        .collect();

    Ok(quote! {
        #[allow(non_camel_case_types)]
        pub type #response_name = #response;

        #[allow(non_camel_case_types)]
        #[derive(Clone)]
        pub struct #request_name #struct_block

        impl #server_path {
            pub async fn #fn_name(&mut self, req: #request_name) -> #response_name {
                self.#internal_fn_name(#call_server_args).await
            }
            async fn #internal_fn_name(#sig) -> #response_name #body
        }

        impl #client_path {
            pub async fn #fn_name(&self, #sig_without_self) -> #response_name {
                #mod_path::#internal_fn_name(self, #request_name { #call_client_args }).await
            }
        }

        #[allow(non_camel_case_types)]
        pub struct #export_name ();
        impl ::servify::ServifyExport for #export_name {
            type Request = #request_name;
            type Response = #response_name;
        }
    })
}

#[cfg(test)]
mod tests {
    use super::impl_export;
    use pretty_assertions::assert_eq;
    use quote::quote;

    #[test]
    fn fail_if_contains_const() {
        assert_eq! {
            impl_export(quote!{}, quote!{
                impl A {
                    const A: usize = 0;
                    fn a(&self) {}
                }
            }).to_string(),
            r#":: core :: compile_error ! { "servify_macro::export cannot handle implementations other than functions." }"#,
        };
    }

    #[test]
    fn fail_if_impl_to_fn() {
        assert_eq! {
            impl_export(quote!{}, quote!{
                fn a() {}
            }).to_string(),
            r#":: core :: compile_error ! { "expected `impl`" }"#
        };
    }

    #[test]
    fn test_export() {
        assert_eq! {
            impl_export(quote!{}, quote!{
                impl SomeStruct {
                    fn increment(&mut self, count: u32) -> u32 {
                        self.count += count;
                        self.count
                    }
                }
            }).to_string(),

            quote!{
                #[allow(non_camel_case_types)]
                pub type __increment_response = u32;

                #[allow(non_camel_case_types)]
                #[derive(Clone)]
                pub struct __increment_request {
                    count: u32
                }

                impl SomeStruct::Server {
                    pub async fn increment(&mut self, req: __increment_request) -> __increment_response {
                        self.__internal_increment(req.count).await
                    }
                    async fn __internal_increment(&mut self, count: u32) -> __increment_response {
                        self.count += count;
                        self.count
                    }
                }

                impl SomeStruct::Client {
                    pub async fn increment(&self, count: u32) -> __increment_response {
                        SomeStruct::__internal_increment(self, __increment_request { count }).await
                    }
                }

                #[allow(non_camel_case_types)]
                pub struct SomeStruct_increment ();
                impl ::servify::ServifyExport for SomeStruct_increment {
                    type Request = __increment_request;
                    type Response = __increment_response;
                }
            }.to_string()
        };
    }
}