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/// # Examples
15///
16/// ```ignore
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    let mut args = None;
184
185    match inputs.first() {
186        Some(FnArg::Receiver(_)) => {}
187        _ => abort!(&method.sig.span(), "first arg must be &self"),
188    };
189
190    inputs
191        .iter()
192        .filter_map(|fn_arg| match fn_arg {
193            FnArg::Receiver(_) => None,
194            FnArg::Typed(ty) => Some((ty, &ty.attrs.first()?.path().segments.first()?.ident)),
195        })
196        .for_each(|(ty, p)| match &*p.to_string() {
197            "path" => path_variables.push(&ty.pat),
198            "query" => querys.push(&ty.pat),
199            "json" => match body {
200                None => body = Some(Json(&ty.pat)),
201                _ => abort!(&ty.span(), "json or form only once"),
202            },
203            "form" => match body {
204                None => body = Some(Form(&ty.pat)),
205                _ => abort!(&ty.span(), "json or form only once"),
206            },
207            "headers" => match headers {
208                None => headers = Some(&ty.pat),
209                _ => abort!(&ty.span(), "headers only once"),
210            },
211            "args" => match args {
212                None => args = Some(&ty.pat),
213                _ => abort!(&ty.span(), "args only once"),
214            },
215            other => abort!(
216                &ty.span(),
217                format!("not allowed param type : {}", other).as_str()
218            ),
219        });
220
221    let path_variables = if path_variables.is_empty() {
222        quote! {}
223    } else {
224        let mut stream = proc_macro2::TokenStream::new();
225        for pv in path_variables {
226            let id = format!("<{}>", quote! {#pv});
227            stream.extend(quote! {
228                .replace(#id, format!("{}", #pv).as_str())
229            });
230        }
231        stream
232    };
233
234    let mut query = if querys.is_empty() {
235        quote! {}
236    } else {
237        quote! {
238            req = req.query(&[#(#querys),*]);
239        }
240    };
241
242    let (mut req_body, mut req_body_enum) = match body {
243        None => (quote! {}, quote! {feign::RequestBody::<()>::None}),
244        Some(Form(form)) => (
245            quote! {
246                req = req.form(#form);
247            },
248            quote! {feign::RequestBody::Form(#form)},
249        ),
250        Some(Json(json)) => (
251            quote! {
252                req = req.json(#json);
253            },
254            quote! {feign::RequestBody::Json(#json)},
255        ),
256    };
257
258    let mut headers = match headers {
259        None => quote! {},
260        Some(headers) => quote! {
261                for header in #headers {
262                    req = req.header(header.0,header.1);
263                }
264        },
265    };
266
267    let mut args_path = quote! {};
268    if let Some(args) = args {
269        args_path = quote! {
270            for path in #args.path() {
271                request_path = request_path.replace(path.0, path.1.as_str());
272            }
273        };
274        query = quote! {
275            if let Some(query) = #args.query() {
276                req = req.query(&query);
277            }
278        };
279        // allready has req_body
280        if body.is_some() {
281            req_body = quote! {
282                #req_body
283                match #args.body() {
284                    feign::RequestBody::None => {},
285                    _ => {
286                        return Err(feign::re_exports::anyhow::anyhow!("json or form can only once"));
287                    },
288                }
289            };
290        } else {
291            req_body = quote! {
292                let req_body = #args.body();
293                match &req_body {
294                    feign::RequestBody::None => {},
295                    feign::RequestBody::Form(form) => {
296                        req = req.form(form);
297                    },
298                    feign::RequestBody::Json(json) => {
299                        req = req.json(json);
300                    },
301                }
302            };
303            req_body_enum = quote! {req_body};
304        }
305        headers = quote! {
306            #headers
307            if let Some(headers) = #args.headers() {
308                for header in headers {
309                    req = req.header(header.0, header.1);
310                }
311            }
312        };
313    };
314
315    let inputs = inputs
316        .iter()
317        .filter_map(|fn_arg| match fn_arg {
318            FnArg::Receiver(_) => None,
319            FnArg::Typed(a) => {
320                let mut a = a.clone();
321                a.attrs.clear();
322                Some(FnArg::Typed(a))
323            }
324        })
325        .collect::<syn::punctuated::Punctuated<_, syn::Token![,]>>();
326
327    let before_send_builder = match before_send {
328        Some(builder) => {
329            let builder_token: proc_macro2::TokenStream = builder.clone().parse().unwrap();
330            quote! {
331                let req = #builder_token(
332                            req,
333                            #req_body_enum,
334                        ).await?;
335            }
336        }
337        None => quote! {},
338    };
339
340    let deserialize = match request.deserialize {
341        None => quote! {::feign::re_exports::serde_json::from_str(text.as_str())},
342        Some(deserialize) => {
343            let builder_token: proc_macro2::TokenStream = deserialize.parse().unwrap();
344            quote! {#builder_token(text).await}
345        }
346    };
347
348    quote! {
349        pub async fn #name(&self, #inputs) #output {
350            let mut request_path = String::from(#req_path)#path_variables;
351            #args_path
352            let url = format!("{}{}{}", self.host, self.path, request_path);
353            let mut req = #reqwest_client_builder
354                        .#http_method_ident(url.as_str());
355            #query
356            #req_body
357            #headers
358            #before_send_builder
359            let text = req
360                .send()
361                .await?
362                .error_for_status()?
363                .text()
364                .await?;
365            Ok(#deserialize?)
366        }
367    }
368}
369
370/// Http methods enumed
371enum HttpMethod {
372    Get,
373    Post,
374    Put,
375    Patch,
376    Delete,
377    Head,
378}
379
380fn http_method_from_ident(ident: &syn::Ident) -> Option<HttpMethod> {
381    Some(match &*ident.to_string() {
382        "get" => HttpMethod::Get,
383        "post" => HttpMethod::Post,
384        "put" => HttpMethod::Put,
385        "patch" => HttpMethod::Patch,
386        "delete" => HttpMethod::Delete,
387        "head" => HttpMethod::Head,
388        _ => return None,
389    })
390}
391
392fn http_method_to_token(method: HttpMethod) -> proc_macro2::TokenStream {
393    match method {
394        HttpMethod::Get => "feign::HttpMethod::Get",
395        HttpMethod::Post => "feign::HttpMethod::Post",
396        HttpMethod::Put => "feign::HttpMethod::Put",
397        HttpMethod::Patch => "feign::HttpMethod::Patch",
398        HttpMethod::Delete => "feign::HttpMethod::Delete",
399        HttpMethod::Head => "feign::HttpMethod::Head",
400    }
401    .parse()
402    .unwrap()
403}
404
405/// body types
406enum RequestBody<'a> {
407    Form(&'a Box<syn::Pat>),
408    Json(&'a Box<syn::Pat>),
409}
410
411/// Args of client
412#[derive(Debug, FromMeta)]
413struct ClientArgs {
414    #[darling(default)]
415    pub host: Option<String>,
416    pub path: String,
417    #[darling(default)]
418    pub client_builder: Option<String>,
419    #[darling(default)]
420    pub before_send: Option<String>,
421}
422
423/// Args of request
424#[derive(Debug, FromMeta)]
425struct Request {
426    pub path: String,
427    #[darling(default)]
428    pub deserialize: Option<String>,
429}
430
431/// Derive macro for the `Args` trait
432///
433/// This macro automatically implements the `Args` trait for a struct,
434/// providing implementations for `request_path` and `request_builder` methods
435/// based on field attributes like `#[path]`, `#[query]`, `#[json]`, `#[form]`, `#[headers]`.
436///
437/// # Examples
438///
439/// ```ignore
440/// use feign::Args;
441///
442/// #[derive(Args)]
443/// struct MyArgs {
444///     #[path]
445///     pub id: i64,
446///     #[query]
447///     pub name: String,
448///     #[json]
449///     pub data: UserData,
450///     #[headers]
451///     pub auth: String,
452/// }
453/// ```
454#[proc_macro_error]
455#[proc_macro_derive(
456    Args,
457    attributes(feigen_path, feigen_query, feigen_json, feigen_form, feigen_headers)
458)]
459pub fn derive_args(input: TokenStream) -> TokenStream {
460    let input = parse_macro_input!(input as syn::DeriveInput);
461    let name = &input.ident;
462
463    let fields = match &input.data {
464        syn::Data::Struct(data) => &data.fields,
465        _ => abort!(
466            &input.ident.span(),
467            "Args derive macro only supports structs"
468        ),
469    };
470
471    let mut path_fields: Vec<(&syn::Ident, &syn::Type)> = Vec::new();
472    let mut query_fields: Vec<(&syn::Ident, &syn::Type)> = Vec::new();
473    let mut json_field: Option<(&syn::Ident, &syn::Type)> = None;
474    let mut form_field: Option<(&syn::Ident, &syn::Type)> = None;
475    let mut headers_field: Option<(&syn::Ident, &syn::Type)> = None;
476
477    for field in fields.iter() {
478        let field_name = field.ident.as_ref().unwrap();
479        let field_type = &field.ty;
480
481        let mut has_path = false;
482        let mut has_query = false;
483        let mut has_json = false;
484        let mut has_form = false;
485        let mut has_headers = false;
486
487        for attr in &field.attrs {
488            if let syn::Meta::Path(path) = &attr.meta {
489                if let Some(ident) = path.get_ident() {
490                    match ident.to_string().as_str() {
491                        "feigen_path" => has_path = true,
492                        "feigen_query" => has_query = true,
493                        "feigen_json" => has_json = true,
494                        "feigen_form" => has_form = true,
495                        "feigen_headers" => has_headers = true,
496                        _ => {}
497                    }
498                }
499            }
500        }
501
502        if has_path {
503            path_fields.push((field_name, field_type));
504        } else if has_query {
505            query_fields.push((field_name, field_type));
506        } else if has_json {
507            match json_field {
508                None => json_field = Some((field_name, field_type)),
509                _ => abort!(&field.span(), "json only once"),
510            }
511        } else if has_form {
512            match form_field {
513                None => form_field = Some((field_name, field_type)),
514                _ => abort!(&field.span(), "form only once"),
515            }
516        } else if has_headers {
517            match headers_field {
518                None => headers_field = Some((field_name, field_type)),
519                _ => abort!(&field.span(), "headers only once"),
520            }
521        }
522    }
523
524    if json_field.is_some() && form_field.is_some() {
525        abort!(&fields.span(), "json or form only once");
526    }
527
528    // Generate request_path method
529    let path = if path_fields.is_empty() {
530        quote! {}
531    } else {
532        let path_pairs: Vec<_> = path_fields
533            .iter()
534            .map(|(field_name, _)| {
535                let id = format!("<{}>", field_name);
536                quote! {
537                    (#id, format!("{}", self.#field_name))
538                }
539            })
540            .collect();
541        quote! {
542            vec![#(#path_pairs),*]
543        }
544    };
545
546    // Generate request_builder method
547    let query = if query_fields.is_empty() {
548        quote! {None}
549    } else {
550        let query_pairs: Vec<_> = query_fields
551            .iter()
552            .map(|(field_name, _)| {
553                quote! {
554                    (stringify!(#field_name), format!("{}", self.#field_name))
555                }
556            })
557            .collect();
558        quote! {
559            Some(vec![#(#query_pairs),*])
560        }
561    };
562
563    let (body, body_type) = match (form_field, json_field) {
564        (Some((field_name, ty)), None) => (
565            quote! {
566                feign::RequestBody::Form(&self.#field_name)
567            },
568            quote! {feign::RequestBody<&#ty>},
569        ),
570        (None, Some((field_name, ty))) => (
571            quote! {
572                feign::RequestBody::Json(&self.#field_name)
573            },
574            quote! {feign::RequestBody<&#ty>},
575        ),
576        _ => (
577            quote! {feign::RequestBody::<()>::None},
578            quote! {feign::RequestBody<()>},
579        ),
580    };
581
582    let (headers, headers_type) = match headers_field {
583        None => (quote! {None}, quote! {Option<()>}),
584        Some((field_name, ty)) => (
585            quote! {
586                Some(&self.#field_name)
587            },
588            quote! {Option<&#ty>},
589        ),
590    };
591
592    let expanded = quote! {
593        impl #name {
594            fn path(&self) -> Vec<(&'static str, String)> {
595                #path
596            }
597
598            fn query(&self) -> Option<Vec<(&'static str, String)>> {
599                #query
600            }
601
602            fn body(&self) -> #body_type {
603                #body
604            }
605
606            fn headers(&self) -> #headers_type {
607                #headers
608            }
609        }
610    };
611
612    TokenStream::from(expanded)
613}