smarty_rust_proc_macro/
lib.rs

1#![allow(clippy::manual_unwrap_or_default)]
2
3extern crate darling;
4extern crate proc_macro;
5
6use darling::{export::NestedMeta, Error, FromMeta};
7use proc_macro::TokenStream;
8use quote::quote;
9use syn::Path;
10
11#[derive(Default, Debug, FromMeta)]
12struct LookupOverride {
13    #[darling(default)]
14    lookup_method: Option<Path>,
15    #[darling(default)]
16    batch_method: Option<Path>,
17}
18
19#[derive(Debug, FromMeta, Default)]
20struct LookupStyle {
21    #[darling(default)]
22    batch: bool,
23    #[darling(default)]
24    lookup: bool,
25    #[darling(default)]
26    overrides: LookupOverride,
27}
28
29impl LookupStyle {
30    fn get_lookup_method(&self) -> Path {
31        match self.overrides.lookup_method.clone() {
32            Some(path) => path,
33            None => Path::from_string("Method::GET").expect("Constant failed to parse"),
34        }
35    }
36
37    fn get_batch_method(&self) -> Path {
38        match self.overrides.batch_method.clone() {
39            Some(path) => path,
40            None => Path::from_string("Method::POST").expect("Constant failed to parse"),
41        }
42    }
43}
44
45#[derive(Debug, FromMeta, Default)]
46struct ResultHandler {
47    #[darling(default)]
48    lookup: bool,
49    #[darling(default)]
50    batch: bool,
51}
52
53#[derive(Debug, FromMeta)]
54struct MacroArgs {
55    default_url: String,
56    api_path: String,
57    lookup_style: LookupStyle,
58    lookup_type: Path,
59    result_type: Path,
60    #[darling(default)]
61    multi_result_type: Option<Path>,
62    #[darling(default)]
63    custom_send: bool,
64    #[darling(default)]
65    result_handler: ResultHandler,
66}
67
68// The main proc macro that allows someone to create a smarty api
69#[proc_macro_attribute]
70pub fn smarty_api(args: TokenStream, input: TokenStream) -> TokenStream {
71    // Turn the attributes into a set of args.
72    let attr_args = match NestedMeta::parse_meta_list(args.into()) {
73        Ok(v) => v,
74        Err(e) => return TokenStream::from(Error::from(e).write_errors()),
75    };
76
77    let mut ast = syn::parse(input).expect("macro input stream should be valid");
78
79    // Generate the arguments in the form of a struct
80    let args = match MacroArgs::from_list(attr_args.as_ref()) {
81        Ok(v) => v,
82        Err(e) => return TokenStream::from(e.write_errors()),
83    };
84
85    impl_smarty_api_macro(&args, &mut ast)
86}
87
88fn impl_smarty_api_macro(attrs: &MacroArgs, ast: &mut syn::DeriveInput) -> TokenStream {
89    // This is the identifier of the struct that this is being implemented on.
90    let name = &ast.ident;
91
92    let default_url = attrs.default_url.clone();
93    let api_path = attrs.api_path.clone();
94
95    let lookup_type = attrs.lookup_type.clone();
96    let result_type = attrs.result_type.clone();
97    let multi_result_type = match attrs.multi_result_type.clone() {
98        Some(r) => r,
99        None => result_type.clone(),
100    };
101
102    // Lets make sure that the API Type has the values it needs.
103    let mut result = quote! {
104        pub struct #name {
105            pub(crate) client: Client
106        }
107
108        impl #name {
109            /// Creates a new client with the given options
110            pub fn new(options: Options) -> Result<Self, SmartyError> {
111                let url = options.url.clone().unwrap_or(#default_url.parse().expect("Parsing Constant should be OK"));
112                Ok(Self {client: Client::new(url, options, #api_path)?})
113            }
114        }
115    };
116
117    let lookup_handler = match attrs.result_handler.lookup {
118        true => {
119            quote! { self.handle_lookup_results(lookup, results); }
120        }
121        false => {
122            quote! {
123                lookup.results = candidates;
124            }
125        }
126    };
127
128    let lookup_method = attrs.lookup_style.get_lookup_method();
129
130    if !attrs.custom_send {
131        if attrs.lookup_style.batch && attrs.lookup_style.lookup {
132            unimplemented!("Only one between batch and lookup allowed for lookup style");
133        } else if attrs.lookup_style.batch {
134            let batch_handler = match attrs.result_handler.batch {
135                true => {
136                    quote! { self.handle_batch_results(batch, results); }
137                }
138                false => {
139                    quote! {
140                        let records = batch.records_mut();
141                        for i in 0..records.len() {
142                            records[i].results = results[i].clone();
143                        }
144                    }
145                }
146            };
147
148            let batch_method = attrs.lookup_style.get_batch_method();
149
150            result = quote! {
151                #result
152
153                impl #name {
154                    /// Uses the lookup and the client in
155                    /// order to build a request and send the message
156                    /// to the server.
157                    async fn send_lookup(&self, lookup: &mut #lookup_type) -> Result<(), SmartyError> {
158                        let mut req = self
159                            .client
160                            .reqwest_client
161                            .request(#lookup_method, self.client.url.clone());
162                        req = self.client.build_request(req);
163                        req = req.query(&lookup.clone().into_param_array());
164
165                        let candidates = send_request::<#result_type>(req).await?;
166
167                        #lookup_handler
168
169                        Ok(())
170                    }
171
172                    /// Uses a batch and the client in
173                    /// Order to build a request
174                    /// that will handle the batch
175                    pub async fn send(&self, batch: &mut Batch<#lookup_type>) -> Result<(), SmartyError> {
176                        if batch.is_empty() {
177                            return Ok(())
178                        }
179
180                        if batch.len() == 1 {
181                            return self.send_lookup(&mut batch.records_mut()[0]).await
182                        }
183
184                        let mut req = self.client.reqwest_client.request(#batch_method, self.client.url.clone());
185                        req = self.client.build_request(req);
186                        req = req.json(batch.records());
187                        req = req.header("Content-Type", "application/json");
188
189                        let results: #multi_result_type = send_request(req).await?;
190
191                        #batch_handler
192
193                        Ok(())
194                    }
195                }
196            }
197        } else if attrs.lookup_style.lookup {
198            result = quote! {
199                #result
200
201                impl #name {
202                    /// Uses the lookup and the client in
203                    /// order to build a request and send the message
204                    /// to the server.
205                    pub async fn send(&self, lookup: &mut #lookup_type) -> Result<(), SmartyError> {
206                        let mut req = self
207                            .client
208                            .reqwest_client
209                            .request(#lookup_method, self.client.url.clone());
210                        req = self.client.build_request(req);
211                        req = req.query(&lookup.clone().into_param_array());
212
213                        let candidates = send_request::<#result_type>(req).await?;
214
215                        #lookup_handler
216
217                        Ok(())
218                    }
219                }
220            }
221        } else {
222            unimplemented!("Add batch or lookup to lookup style");
223        }
224    }
225
226    result.into()
227}