forward-methods 0.0.2

A derive macro for forwarding methods from composed objects
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Pat, PatType, Path, Receiver, ReturnType, Type};

use crate::model::{Delegate, FwdDecl, Method};

impl FwdDecl {
    pub fn implement(&self) -> TokenStream {
        let impls = self.implement_delegates();

        quote!(#(#impls) *)
    }

    pub fn implement_pub(&self) -> TokenStream {
        let impls = self.implement_delegates();

        quote!(#(pub #impls) *)
    }

    fn implement_delegates(&self) -> Vec<TokenStream> {
        match &self.delegate {
            Delegate::MethodList(meths) => meths.iter().map(|meth| {
                let member = &self.target;
                let name = &meth.ident;
                let args = quote_args(&meth.rcv, &meth.args);
                let arg_names = quote_arg_names(&meth.args);
                let ret = &meth.ret;

                let clone = if is_rcv_ref(meth) && is_ret_val_not_option(meth) {
                    quote!(.clone())
                } else {
                    quote!()
                };

                quote!(fn #name(#args) #ret { self.#member.#name(#arg_names)#clone })
            }),
        }
        .collect()
    }
}

fn quote_args(rcv: &Receiver, args: &Vec<PatType>) -> TokenStream {
    if args.is_empty() {
        quote!(#rcv)
    } else {
        quote!(#rcv, #(#args),*)
    }
}

fn quote_arg_names(args: &Vec<PatType>) -> TokenStream {
    let pats: Vec<Box<Pat>> = args.iter().map(|x| x.pat.clone()).collect();
    quote!(#(#pats),*)
}

fn is_rcv_ref(meth: &Method) -> bool {
    meth.rcv.reference.is_some()
}

fn is_ret_val_not_option(meth: &Method) -> bool {
    if let ReturnType::Type(_, typ) = meth.ret.clone() {
        !is_option_type(typ.as_ref())
    } else {
        true
    }
}

fn is_option_type(typ: &Type) -> bool {
    match typ {
        Type::Path(x) => has_option_token(&x.path),
        Type::Reference(x) => is_option_type(x.elem.as_ref()),
        _ => false,
    }
}

fn has_option_token(path: &Path) -> bool {
    path.segments.iter().any(|x| x.ident == "Option")
}

#[cfg(test)]
mod tests {
    use proc_macro2::TokenStream;
    use quote::quote;
    use test_case::test_case;

    use crate::model::{FwdDeclBuilder, MethodBuilder};

    #[test_case(
        FwdDeclBuilder::default().named_target("tester").with_method(MethodBuilder::default().ident("test").rcv()),
        quote!(fn test(self) { self.tester.test() });
        "should implement method forwarding to named member"
    )]
    #[test_case(
        FwdDeclBuilder::default().unnamed_target(42).with_method(MethodBuilder::default().ident("test").rcv()),
        quote!(fn test(self) { self.42.test() });
        "should implement method forwarding to unnamed member"
    )]
    #[test_case(
        FwdDeclBuilder::default().unnamed_target(42).with_method(
            MethodBuilder::default().ident("test").with_arg("arg1: u8").with_arg("arg2: &str").rcv()
        ),
        quote!(fn test(self, arg1: u8, arg2: &str) { self.42.test(arg1, arg2) });
        "should implement method with arguments forwarding to unnamed member"
    )]
    #[test_case(
        FwdDeclBuilder::default().unnamed_target(42).with_method(
            MethodBuilder::default().ident("test").rcv().ret("-> String")
        ),
        quote!(fn test(self) -> String { self.42.test() });
        "should implement method with return value forwarding to unnamed member"
    )]
    #[test_case(
        FwdDeclBuilder::default().unnamed_target(42).with_method(
            MethodBuilder::default().ident("test").ref_rcv().ret("-> String")
        ),
        quote!(fn test(&self) -> String { self.42.test().clone() });
        "should implement method with return value forwarding to unnamed member reference"
    )]
    #[test_case(
        FwdDeclBuilder::default().unnamed_target(42).with_method(
            MethodBuilder::default().ident("test").ref_rcv().ret("-> Option<String>")
        ),
        quote!(fn test(&self) -> Option<String> { self.42.test() });
        "should implement method with option return value forwarding to unnamed member reference"
    )]
    #[test_case(
        FwdDeclBuilder::default().unnamed_target(42).with_method(
            MethodBuilder::default().ident("test").ref_rcv().ret("-> &Option<String>")
        ),
        quote!(fn test(&self) -> &Option<String> { self.42.test() });
        "should implement method with option reference return value forwarding to unnamed member reference"
    )]
    #[test_case(
        FwdDeclBuilder::default().unnamed_target(42).with_method(
            MethodBuilder::default().ident("test").ref_rcv().ret("-> &mut Option<String>")
        ),
        quote!(fn test(&self) -> &mut Option<String> { self.42.test() });
        "should implement method with mutable option reference return value forwarding to unnamed member reference"
    )]
    fn should_write_forwarding_impl(input: &FwdDeclBuilder, want: TokenStream) {
        let decl = input.build().unwrap();

        assert_eq!(decl.implement().to_string(), want.to_string())
    }

    #[test]
    fn should_write_public_forwarding_impl() {
        let decls = FwdDeclBuilder::default()
            .named_target("tester")
            .with_method(MethodBuilder::default().ident("test1").rcv())
            .with_method(MethodBuilder::default().ident("test2").rcv())
            .build()
            .unwrap();

        let want = quote!(
            pub fn test1(self) {
                self.tester.test1()
            }
            pub fn test2(self) {
                self.tester.test2()
            }
        );

        assert_eq!(decls.implement_pub().to_string(), want.to_string())
    }
}