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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
extern crate darling;
extern crate proc_macro;

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

#[derive(Default, Debug, FromMeta)]
struct LookupOverride {
    #[darling(default)]
    lookup_method: Option<Path>,
    #[darling(default)]
    batch_method: Option<Path>,
}

#[derive(Debug, FromMeta, Default)]
struct LookupStyle {
    #[darling(default)]
    batch: bool,
    #[darling(default)]
    lookup: bool,
    #[darling(default)]
    overrides: LookupOverride,
}

impl LookupStyle {
    fn get_lookup_method(&self) -> Path {
        match self.overrides.lookup_method.clone() {
            Some(path) => path,
            None => Path::from_string("Method::GET").expect("Constant failed to parse"),
        }
    }

    fn get_batch_method(&self) -> Path {
        match self.overrides.batch_method.clone() {
            Some(path) => path,
            None => Path::from_string("Method::POST").expect("Constant failed to parse"),
        }
    }
}

#[derive(Debug, FromMeta, Default)]
struct ResultHandler {
    #[darling(default)]
    lookup: bool,
    #[darling(default)]
    batch: bool,
}

#[derive(Debug, FromMeta)]
struct MacroArgs {
    default_url: String,
    api_path: String,
    lookup_style: LookupStyle,
    lookup_type: Path,
    result_type: Path,
    #[darling(default)]
    multi_result_type: Option<Path>,
    #[darling(default)]
    custom_send: bool,
    #[darling(default)]
    result_handler: ResultHandler,
}

// The main proc macro that allows someone to create a smarty api
#[proc_macro_attribute]
pub fn smarty_api(args: TokenStream, input: TokenStream) -> TokenStream {
    // Turn the attributes into a set of args.
    let attr_args = match NestedMeta::parse_meta_list(args.into()) {
        Ok(v) => v,
        Err(e) => return TokenStream::from(Error::from(e).write_errors()),
    };

    let mut ast = syn::parse(input).expect("macro input stream should be valid");

    // Generate the arguments in the form of a struct
    let args = match MacroArgs::from_list(attr_args.as_ref()) {
        Ok(v) => v,
        Err(e) => return TokenStream::from(e.write_errors()),
    };

    impl_smarty_api_macro(&args, &mut ast)
}

fn impl_smarty_api_macro(attrs: &MacroArgs, ast: &mut syn::DeriveInput) -> TokenStream {
    // This is the identifier of the struct that this is being implemented on.
    let name = &ast.ident;

    let default_url = attrs.default_url.clone();
    let api_path = attrs.api_path.clone();

    let lookup_type = attrs.lookup_type.clone();
    let result_type = attrs.result_type.clone();
    let multi_result_type = match attrs.multi_result_type.clone() {
        Some(r) => r,
        None => result_type.clone(),
    };

    // Lets make sure that the API Type has the values it needs.
    let mut result = quote! {

    pub struct #name {
        pub(crate) client: Client
    }

    impl #name {
        /// Creates a new client with the given options
        pub fn new(options: Options) -> Result<Self, ParseError> {
            Self::new_custom_base_url(#default_url.parse()?, options)
        }

        /// Creates a new client with the given options that points to a different url.
        pub fn new_custom_base_url(base_url: Url, options: Options) -> Result<Self, ParseError> {
            Ok(Self {client: Client::new(base_url, options, #api_path)?})
        }
    }

    };

    let lookup_handler = match attrs.result_handler.lookup {
        true => {
            quote! { self.handle_lookup_results(lookup, results); }
        }
        false => {
            quote! {
                lookup.results = candidates;
            }
        }
    };

    let lookup_method = attrs.lookup_style.get_lookup_method();

    if !attrs.custom_send {
        if attrs.lookup_style.batch && attrs.lookup_style.lookup {
            unimplemented!("Only one between batch and lookup allowed for lookup style");
        } else if attrs.lookup_style.batch {
            let batch_handler = match attrs.result_handler.batch {
                true => {
                    quote! { self.handle_batch_results(batch, results); }
                }
                false => {
                    quote! {
                        let records = batch.records_mut();
                        for i in 0..records.len() {
                            records[i].results = results[i].clone();
                        }
                    }
                }
            };

            let batch_method = attrs.lookup_style.get_batch_method();

            result = quote! {
                #result

                impl #name {
                    /// Uses the lookup and the client in
                    /// order to build a request and send the message
                    /// to the server.
                    async fn send_lookup(&self, lookup: &mut #lookup_type) -> Result<(), SDKError> {
                        let mut req = self
                            .client
                            .reqwest_client
                            .request(#lookup_method, self.client.url.clone());
                        req = self.client.build_request(req);
                        req = req.query(&lookup.clone().into_param_array());

                        let candidates = send_request::<#result_type>(req).await?;

                        #lookup_handler

                        Ok(())
                    }

                    /// Uses a batch and the client in
                    /// Order to build a request
                    /// that will handle the batch
                    pub async fn send(&self, batch: &mut Batch<#lookup_type>) -> Result<(), SDKError> {
                        if batch.is_empty() {
                            return Ok(())
                        }

                        if batch.len() == 1 {
                            return self.send_lookup(&mut batch.records_mut()[0]).await
                        }

                        let mut req = self.client.reqwest_client.request(#batch_method, self.client.url.clone());
                        req = self.client.build_request(req);
                        req = req.json(batch.records());
                        req = req.header("Content-Type", "application/json");

                        let results: #multi_result_type = send_request(req).await?;

                        #batch_handler

                        Ok(())
                    }
                }
            }
        } else if attrs.lookup_style.lookup {
            result = quote! {
                #result

                impl #name {
                    /// Uses the lookup and the client in
                    /// order to build a request and send the message
                    /// to the server.
                    pub async fn send(&self, lookup: &mut #lookup_type) -> Result<(), SDKError> {
                        let mut req = self
                            .client
                            .reqwest_client
                            .request(#lookup_method, self.client.url.clone());
                        req = self.client.build_request(req);
                        req = req.query(&lookup.clone().into_param_array());

                        let candidates = send_request::<#result_type>(req).await?;

                        #lookup_handler

                        Ok(())
                    }
                }
            }
        } else {
            unimplemented!("Add batch or lookup to lookup style");
        }
    }

    result.into()
}