1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
extern crate proc_macro;

mod create;
mod methods;
mod self_type;

use darling::{export::NestedMeta, FromMeta};
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_attribute]
pub fn create(args: TokenStream, original: TokenStream) -> TokenStream {
    let original = syn::parse_macro_input!(original as syn::ItemStruct);

    let args = match NestedMeta::parse_meta_list(args.into())
        .map_err(darling::Error::from)
        .and_then(|v| create::Args::from_list(&v))
    {
        Ok(v) => v,
        Err(e) => return e.write_errors().into(),
    };

    let mockable = create::Mockable::new(original, args);

    TokenStream::from(mockable)
}

#[proc_macro_attribute]
pub fn methods(args: TokenStream, original: TokenStream) -> TokenStream {
    let original = syn::parse_macro_input!(original as syn::ItemImpl);

    let args = match NestedMeta::parse_meta_list(args.into())
        .map_err(darling::Error::from)
        .and_then(|v| methods::Args::from_list(&v))
    {
        Ok(v) => v,
        Err(e) => return e.write_errors().into(),
    };

    match methods::Mockable::new(original, args) {
        Ok(mockable) => TokenStream::from(mockable),
        Err(e) => e.write_errors().into(),
    }
}

#[proc_macro]
pub fn when(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    match syn::parse_macro_input!(input as syn::Expr) {
        syn::Expr::Field(syn::ExprField {
            base,
            member: syn::Member::Named(ident),
            ..
        }) => {
            let when = quote::format_ident!("_when_{}", ident);
            TokenStream::from(quote!( { #base.#when() }))
        }
        syn::Expr::MethodCall(syn::ExprMethodCall {
            receiver,
            method,
            args,
            ..
        }) => {
            let when = quote::format_ident!("_when_{}", method);

            let args = args
                .into_iter()
                .map(expr_to_matcher)
                .collect::<Result<Vec<_>, _>>();

            match args {
                Err(e) => e.write_errors().into(),
                Ok(args) => TokenStream::from(quote!({ #receiver.#when().with_args((#(#args,)*)) }))
            }
        }
        expr => darling::Error::custom("faux::when! only accepts arguments in the format of: `when!(receiver.method)` or `receiver.method(args...)`")
             .with_span(&expr)
             .write_errors()
             .into(),
    }
}

use quote::ToTokens;

fn ref_matcher_maybe(
    expr: &syn::Expr,
    left: &syn::Expr,
    matcher: impl FnOnce() -> darling::Result<proc_macro2::TokenStream>,
) -> darling::Result<proc_macro2::TokenStream> {
    match left {
        syn::Expr::Infer(_) => matcher(),
        syn::Expr::Unary(syn::ExprUnary {
            op: syn::UnOp::Deref(_),
            expr,
            ..
        }) => {
            let matcher = matcher()?;
            Ok(quote! { faux::matcher::ArgMatcher::<#expr>::into_ref_matcher(#matcher) })
        }
        _ => Ok(quote! { faux::matcher::eq(#expr) }),
    }
}

fn expr_to_matcher(expr: syn::Expr) -> darling::Result<proc_macro2::TokenStream> {
    match &expr {
        syn::Expr::Infer(_) => Ok(quote! { faux::matcher::any() }),
        syn::Expr::Assign(syn::ExprAssign { left, right, .. }) => {
            ref_matcher_maybe(&expr, left, || Ok(right.to_token_stream()))
        }
        syn::Expr::Binary(syn::ExprBinary {
            left, op, right, ..
        }) => ref_matcher_maybe(&expr, left, || match op {
            syn::BinOp::Eq(_) => Ok(quote! { faux::matcher::eq_against(#right) }),
            _ => Err(darling::Error::custom(format!(
                "faux:when! does not handle argument matchers with syntax: '{}'",
                expr.to_token_stream()
            ))
            .with_span(&expr)),
        }),
        arg => Ok(quote! { faux::matcher::eq(#arg) }),
    }
}