feign_macros/
lib.rs

1extern crate proc_macro;
2
3use crate::RequestBody::{Form, Json};
4use darling::ast::NestedMeta;
5use darling::{Error, FromMeta};
6use proc_macro::TokenStream;
7use proc_macro_error::{abort, proc_macro_error};
8use quote::quote;
9use syn::spanned::Spanned;
10use syn::{parse_macro_input, FnArg, TraitItemFn};
11
12/// Make a restful http client
13///
14/// # Examlples
15///
16/// ```
17/// #[client(host = "http://127.0.0.1:3000", path = "/user")]
18/// pub trait UserClient {
19///     #[get(path = "/find_by_id/<id>")]
20///     async fn find_by_id(&self, #[path] id: i64) -> ClientResult<Option<User>>;
21///    #[post(path = "/new_user")]
22///     async fn new_user(&self, #[json] user: &User) -> Result<Option<String>, Box<dyn std::error::Error>>;
23/// }
24/// ```
25///
26#[proc_macro_error]
27#[proc_macro_attribute]
28pub fn client(args: TokenStream, input: TokenStream) -> TokenStream {
29    let args = match NestedMeta::parse_meta_list(args.into()) {
30        Ok(v) => v,
31        Err(e) => {
32            return TokenStream::from(Error::from(e).write_errors());
33        }
34    };
35    let input = parse_macro_input!(input as syn::ItemTrait);
36    let args: ClientArgs = match ClientArgs::from_list(&args) {
37        Ok(v) => v,
38        Err(e) => return TokenStream::from(e.write_errors()),
39    };
40
41    let reqwest_client_builder = match args.client_builder {
42        Some(builder) => {
43            let builder_token: proc_macro2::TokenStream = builder.parse().unwrap();
44            quote! {
45                #builder_token().await?
46            }
47        }
48        None => quote! {
49            ::feign::re_exports::reqwest::ClientBuilder::new().build()?
50        },
51    };
52
53    let vis = &input.vis;
54    let name = &input.ident;
55    let base_host = &match args.host {
56        None => String::from(""),
57        Some(value) => value,
58    };
59    let base_path = &args.path;
60
61    let methods = input
62        .items
63        .iter()
64        .filter_map(|item| match item {
65            syn::TraitItem::Fn(m) => Some(m),
66            _ => None,
67        })
68        .map(|m| gen_method(m, args.before_send.as_ref(), &reqwest_client_builder));
69
70    let builder_name: proc_macro2::TokenStream =
71        format!("{}Builder", quote! {#name}).parse().unwrap();
72
73    let tokens = quote! {
74
75        #[derive(Debug, Clone)]
76        #vis struct #name {
77            host: String,
78            path: String,
79        }
80
81        impl #name {
82
83            pub fn new() -> Self {
84                Self{
85                    host: String::from(#base_host),
86                    path: String::from(#base_path),
87                }
88            }
89
90            pub fn new_with_builder(host: String) -> Self {
91                Self{
92                    host: host,
93                    path: String::from(#base_path),
94                }
95            }
96
97            pub fn builder() -> #builder_name {
98                #builder_name::new()
99            }
100
101            #(#methods)*
102        }
103
104        #vis struct #builder_name {
105            host: String,
106        }
107
108        impl #builder_name {
109
110            pub fn new() -> Self {
111                Self{
112                    host: String::from(#base_host),
113                }
114            }
115
116            pub fn build(&self) -> #name {
117                #name::new_with_builder(self.host.clone())
118            }
119
120            pub fn set_host(mut self, host: String) -> Self {
121                self.host = host;
122                self
123            }
124
125        }
126
127    };
128
129    tokens.into()
130}
131
132/// Gen feign methods
133fn gen_method(
134    method: &TraitItemFn,
135    before_send: Option<&String>,
136    reqwest_client_builder: &proc_macro2::TokenStream,
137) -> proc_macro2::TokenStream {
138    if method.sig.asyncness.is_none() {
139        abort!(
140            &method.sig.span(),
141            "Non-asynchronous calls are not currently supported"
142        )
143    }
144
145    let name = &method.sig.ident;
146    let inputs = &method.sig.inputs;
147    let output = &method.sig.output;
148    let attr = method.attrs.iter().next();
149    let http_method_ident = match attr.map(|a| a.path().get_ident()).flatten() {
150        Some(ident) => ident,
151        None => {
152            abort!(&method.span(), "Expects an http method")
153        }
154    };
155
156    let _http_method = if let Some(m) = http_method_from_ident(http_method_ident) {
157        m
158    } else {
159        abort!(
160            &http_method_ident.span(),
161            "Expect one of get, post, put, patch, delete, head."
162        )
163    };
164
165    let _http_method_token = http_method_to_token(_http_method);
166
167    let request: Request = match Request::from_meta(&attr.unwrap().meta) {
168        Ok(v) => v,
169        Err(err) => return TokenStream::from(err.write_errors()).into(),
170    };
171
172    let req_path = &request.path;
173
174    let mut path_variables = Vec::new();
175    let mut querys = Vec::new();
176    let mut body = None;
177    let mut headers = None;
178
179    match inputs.first() {
180        Some(FnArg::Receiver(_)) => {}
181        _ => abort!(&method.sig.span(), "first arg must be &self"),
182    };
183
184    inputs
185        .iter()
186        .filter_map(|fn_arg| match fn_arg {
187            FnArg::Receiver(_) => None,
188            FnArg::Typed(ty) => Some((ty, &ty.attrs.first()?.path().segments.first()?.ident)),
189        })
190        .for_each(|(ty, p)| match &*p.to_string() {
191            "path" => path_variables.push(&ty.pat),
192            "query" => querys.push(&ty.pat),
193            "json" => match body {
194                None => body = Some(Json(&ty.pat)),
195                _ => abort!(&ty.span(), "json or form only once"),
196            },
197            "form" => match body {
198                None => body = Some(Form(&ty.pat)),
199                _ => abort!(&ty.span(), "json or form only once"),
200            },
201            "headers" => match headers {
202                None => headers = Some(&ty.pat),
203                _ => abort!(&ty.span(), "json or form only once"),
204            },
205            other => abort!(
206                &ty.span(),
207                format!("not allowed param type : {}", other).as_str()
208            ),
209        });
210
211    let path_variables = if path_variables.is_empty() {
212        quote! {}
213    } else {
214        let mut stream = proc_macro2::TokenStream::new();
215        for pv in path_variables {
216            let id = format!("<{}>", quote! {#pv});
217            stream.extend(quote! {
218                .replace(#id, format!("{}", #pv).as_str())
219            });
220        }
221        stream
222    };
223
224    let query = if querys.is_empty() {
225        quote! {}
226    } else {
227        quote! {
228            .query(&[#(#querys),*])
229        }
230    };
231
232    let req_body = match body {
233        None => quote! {},
234        Some(Form(form)) => quote! {
235            .form(#form)
236        },
237        Some(Json(json)) => quote! {
238            .json(#json)
239        },
240    };
241
242    let request_builder_body = match body {
243        None => quote! {
244            feign::RequestBody::None
245        },
246        Some(Form(form)) => quote! {
247            feign::RequestBody::Json(::feign::re_exports::serde_json::to_value(#form)?)
248        },
249        Some(Json(json)) => quote! {
250            feign::RequestBody::Json(::feign::re_exports::serde_json::to_value(#json)?)
251        },
252    };
253
254    let header_mut: proc_macro2::TokenStream = match headers {
255        None => quote! {},
256        Some(_) => quote! {mut},
257    };
258
259    let headers_point = match headers {
260        None => quote! {None},
261        Some(headers) => quote! {Some(#headers)},
262    };
263
264    let headers = match headers {
265        None => quote! {},
266        Some(headers) => quote! {
267            for header in #headers.clone() {
268                req = req.header(header.0,header.1);
269            }
270        },
271    };
272
273    let params = quote! {
274        #query
275        #req_body
276    };
277
278    let inputs = inputs
279        .iter()
280        .filter_map(|fn_arg| match fn_arg {
281            FnArg::Receiver(_) => None,
282            FnArg::Typed(a) => {
283                let mut a = a.clone();
284                a.attrs.clear();
285                Some(FnArg::Typed(a))
286            }
287        })
288        .collect::<syn::punctuated::Punctuated<_, syn::Token![,]>>();
289
290    let before_send_builder = match before_send {
291        Some(builder) => {
292            let builder_token: proc_macro2::TokenStream = builder.clone().parse().unwrap();
293            quote! {
294                #builder_token(
295                            req,
296                            #_http_method_token,
297                            self.host.clone(),
298                            self.path.clone(),
299                            request_path.clone(),
300                            #request_builder_body,
301                            #headers_point,
302                        ).await?
303            }
304        }
305        None => quote! {
306            req
307        },
308    };
309
310    let deserialize = match request.deserialize {
311        None => quote! {::feign::re_exports::serde_json::from_str(text.as_str())},
312        Some(deserialize) => {
313            let builder_token: proc_macro2::TokenStream = deserialize.parse().unwrap();
314            quote! {#builder_token(text).await}
315        }
316    };
317
318    quote! {
319        pub async fn #name(&self, #inputs) #output {
320            let request_path = String::from(#req_path)#path_variables;
321            let url = format!("{}{}{}", self.host, self.path, request_path);
322            let #header_mut req = #reqwest_client_builder
323                        .#http_method_ident(url.as_str())
324                        #params;
325            #headers;
326            let req = #before_send_builder;
327            let text = req
328                .send()
329                .await?
330                .error_for_status()?
331                .text()
332                .await?;
333            Ok(#deserialize?)
334        }
335    }
336}
337
338/// Http methods enumed
339enum HttpMethod {
340    Get,
341    Post,
342    Put,
343    Patch,
344    Delete,
345    Head,
346}
347
348fn http_method_from_ident(ident: &syn::Ident) -> Option<HttpMethod> {
349    Some(match &*ident.to_string() {
350        "get" => HttpMethod::Get,
351        "post" => HttpMethod::Post,
352        "put" => HttpMethod::Put,
353        "patch" => HttpMethod::Patch,
354        "delete" => HttpMethod::Delete,
355        "head" => HttpMethod::Head,
356        _ => return None,
357    })
358}
359
360fn http_method_to_token(method: HttpMethod) -> proc_macro2::TokenStream {
361    match method {
362        HttpMethod::Get => "feign::HttpMethod::Get",
363        HttpMethod::Post => "feign::HttpMethod::Post",
364        HttpMethod::Put => "feign::HttpMethod::Put",
365        HttpMethod::Patch => "feign::HttpMethod::Patch",
366        HttpMethod::Delete => "feign::HttpMethod::Delete",
367        HttpMethod::Head => "feign::HttpMethod::Head",
368    }
369    .parse()
370    .unwrap()
371}
372
373/// body types
374enum RequestBody<'a> {
375    Form(&'a Box<syn::Pat>),
376    Json(&'a Box<syn::Pat>),
377}
378
379/// Args of client
380#[derive(Debug, FromMeta)]
381struct ClientArgs {
382    #[darling(default)]
383    pub host: Option<String>,
384    pub path: String,
385    #[darling(default)]
386    pub client_builder: Option<String>,
387    #[darling(default)]
388    pub before_send: Option<String>,
389}
390
391/// Args of request
392#[derive(Debug, FromMeta)]
393struct Request {
394    pub path: String,
395    #[darling(default)]
396    pub deserialize: Option<String>,
397}