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)]
76        #vis struct #name {
77            host: std::sync::Arc<dyn feign::Host>,
78            path: String,
79        }
80
81        impl #name {
82
83            pub fn new() -> Self {
84                Self{
85                    host: std::sync::Arc::new(String::from(#base_host)),
86                    path: String::from(#base_path),
87                }
88            }
89
90            pub fn new_with_builder(host: std::sync::Arc<dyn feign::Host>) -> Self {
91                Self{
92                    host,
93                    path: String::from(#base_path).into(),
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: std::sync::Arc<dyn feign::Host>,
106        }
107
108        impl #builder_name {
109
110            pub fn new() -> Self {
111                Self{
112                    host: std::sync::Arc::new(String::from(#base_host)),
113                }
114            }
115
116            pub fn build(self) -> #name {
117                #name::new_with_builder(self.host)
118            }
119
120            pub fn set_host(mut self, host: impl ::feign::Host) -> Self {
121                self.host = std::sync::Arc::new(host);
122                self
123            }
124
125            pub fn set_host_arc(mut self, host: std::sync::Arc<dyn ::feign::Host>) -> Self {
126                self.host = host;
127                self
128            }
129
130        }
131
132    };
133
134    tokens.into()
135}
136
137/// Gen feign methods
138fn gen_method(
139    method: &TraitItemFn,
140    before_send: Option<&String>,
141    reqwest_client_builder: &proc_macro2::TokenStream,
142) -> proc_macro2::TokenStream {
143    if method.sig.asyncness.is_none() {
144        abort!(
145            &method.sig.span(),
146            "Non-asynchronous calls are not currently supported"
147        )
148    }
149
150    let name = &method.sig.ident;
151    let inputs = &method.sig.inputs;
152    let output = &method.sig.output;
153    let attr = method.attrs.iter().next();
154    let http_method_ident = match attr.map(|a| a.path().get_ident()).flatten() {
155        Some(ident) => ident,
156        None => {
157            abort!(&method.span(), "Expects an http method")
158        }
159    };
160
161    let _http_method = if let Some(m) = http_method_from_ident(http_method_ident) {
162        m
163    } else {
164        abort!(
165            &http_method_ident.span(),
166            "Expect one of get, post, put, patch, delete, head."
167        )
168    };
169
170    let _http_method_token = http_method_to_token(_http_method);
171
172    let request: Request = match Request::from_meta(&attr.unwrap().meta) {
173        Ok(v) => v,
174        Err(err) => return TokenStream::from(err.write_errors()).into(),
175    };
176
177    let req_path = &request.path;
178
179    let mut path_variables = Vec::new();
180    let mut querys = Vec::new();
181    let mut body = None;
182    let mut headers = None;
183
184    match inputs.first() {
185        Some(FnArg::Receiver(_)) => {}
186        _ => abort!(&method.sig.span(), "first arg must be &self"),
187    };
188
189    inputs
190        .iter()
191        .filter_map(|fn_arg| match fn_arg {
192            FnArg::Receiver(_) => None,
193            FnArg::Typed(ty) => Some((ty, &ty.attrs.first()?.path().segments.first()?.ident)),
194        })
195        .for_each(|(ty, p)| match &*p.to_string() {
196            "path" => path_variables.push(&ty.pat),
197            "query" => querys.push(&ty.pat),
198            "json" => match body {
199                None => body = Some(Json(&ty.pat)),
200                _ => abort!(&ty.span(), "json or form only once"),
201            },
202            "form" => match body {
203                None => body = Some(Form(&ty.pat)),
204                _ => abort!(&ty.span(), "json or form only once"),
205            },
206            "headers" => match headers {
207                None => headers = Some(&ty.pat),
208                _ => abort!(&ty.span(), "json or form only once"),
209            },
210            other => abort!(
211                &ty.span(),
212                format!("not allowed param type : {}", other).as_str()
213            ),
214        });
215
216    let path_variables = if path_variables.is_empty() {
217        quote! {}
218    } else {
219        let mut stream = proc_macro2::TokenStream::new();
220        for pv in path_variables {
221            let id = format!("<{}>", quote! {#pv});
222            stream.extend(quote! {
223                .replace(#id, format!("{}", #pv).as_str())
224            });
225        }
226        stream
227    };
228
229    let query = if querys.is_empty() {
230        quote! {}
231    } else {
232        quote! {
233            .query(&[#(#querys),*])
234        }
235    };
236
237    let req_body = match body {
238        None => quote! {},
239        Some(Form(form)) => quote! {
240            .form(#form)
241        },
242        Some(Json(json)) => quote! {
243            .json(#json)
244        },
245    };
246
247    let request_builder_body = match body {
248        None => quote! {
249            feign::RequestBody::None
250        },
251        Some(Form(form)) => quote! {
252            feign::RequestBody::Form(::feign::re_exports::serde_json::to_value(#form)?)
253        },
254        Some(Json(json)) => quote! {
255            feign::RequestBody::Json(::feign::re_exports::serde_json::to_value(#json)?)
256        },
257    };
258
259    let header_mut: proc_macro2::TokenStream = match headers {
260        None => quote! {},
261        Some(_) => quote! {mut},
262    };
263
264    let headers_point = match headers {
265        None => quote! {None},
266        Some(headers) => quote! {Some(#headers)},
267    };
268
269    let headers = match headers {
270        None => quote! {},
271        Some(headers) => quote! {
272            for header in #headers.clone() {
273                req = req.header(header.0,header.1);
274            }
275        },
276    };
277
278    let params = quote! {
279        #query
280        #req_body
281    };
282
283    let inputs = inputs
284        .iter()
285        .filter_map(|fn_arg| match fn_arg {
286            FnArg::Receiver(_) => None,
287            FnArg::Typed(a) => {
288                let mut a = a.clone();
289                a.attrs.clear();
290                Some(FnArg::Typed(a))
291            }
292        })
293        .collect::<syn::punctuated::Punctuated<_, syn::Token![,]>>();
294
295    let before_send_builder = match before_send {
296        Some(builder) => {
297            let builder_token: proc_macro2::TokenStream = builder.clone().parse().unwrap();
298            quote! {
299                #builder_token(
300                            req,
301                            #_http_method_token,
302                            self.host.host().to_string(),
303                            self.path.clone(),
304                            request_path.clone(),
305                            #request_builder_body,
306                            #headers_point,
307                        ).await?
308            }
309        }
310        None => quote! {
311            req
312        },
313    };
314
315    let deserialize = match request.deserialize {
316        None => quote! {::feign::re_exports::serde_json::from_str(text.as_str())},
317        Some(deserialize) => {
318            let builder_token: proc_macro2::TokenStream = deserialize.parse().unwrap();
319            quote! {#builder_token(text).await}
320        }
321    };
322
323    quote! {
324        pub async fn #name(&self, #inputs) #output {
325            let request_path = String::from(#req_path)#path_variables;
326            let url = format!("{}{}{}", self.host, self.path, request_path);
327            let #header_mut req = #reqwest_client_builder
328                        .#http_method_ident(url.as_str())
329                        #params;
330            #headers;
331            let req = #before_send_builder;
332            let text = req
333                .send()
334                .await?
335                .error_for_status()?
336                .text()
337                .await?;
338            Ok(#deserialize?)
339        }
340    }
341}
342
343/// Http methods enumed
344enum HttpMethod {
345    Get,
346    Post,
347    Put,
348    Patch,
349    Delete,
350    Head,
351}
352
353fn http_method_from_ident(ident: &syn::Ident) -> Option<HttpMethod> {
354    Some(match &*ident.to_string() {
355        "get" => HttpMethod::Get,
356        "post" => HttpMethod::Post,
357        "put" => HttpMethod::Put,
358        "patch" => HttpMethod::Patch,
359        "delete" => HttpMethod::Delete,
360        "head" => HttpMethod::Head,
361        _ => return None,
362    })
363}
364
365fn http_method_to_token(method: HttpMethod) -> proc_macro2::TokenStream {
366    match method {
367        HttpMethod::Get => "feign::HttpMethod::Get",
368        HttpMethod::Post => "feign::HttpMethod::Post",
369        HttpMethod::Put => "feign::HttpMethod::Put",
370        HttpMethod::Patch => "feign::HttpMethod::Patch",
371        HttpMethod::Delete => "feign::HttpMethod::Delete",
372        HttpMethod::Head => "feign::HttpMethod::Head",
373    }
374    .parse()
375    .unwrap()
376}
377
378/// body types
379enum RequestBody<'a> {
380    Form(&'a Box<syn::Pat>),
381    Json(&'a Box<syn::Pat>),
382}
383
384/// Args of client
385#[derive(Debug, FromMeta)]
386struct ClientArgs {
387    #[darling(default)]
388    pub host: Option<String>,
389    pub path: String,
390    #[darling(default)]
391    pub client_builder: Option<String>,
392    #[darling(default)]
393    pub before_send: Option<String>,
394}
395
396/// Args of request
397#[derive(Debug, FromMeta)]
398struct Request {
399    pub path: String,
400    #[darling(default)]
401    pub deserialize: Option<String>,
402}