Skip to main content

utoipa_ts_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::{format_ident, quote};
4use syn::{
5    Expr, Ident, ItemFn, LitStr, Result, Token, Type, parenthesized,
6    parse::{Parse, ParseStream},
7    parse_macro_input,
8};
9
10/// Registers a [`utoipa`] endpoint for TypeScript API generation
11///
12/// This macro forwards all arguments to [`utoipa::path`] and additionally records
13/// supported endpoint metadata for [`utoipa_ts::export!`]
14///
15/// [`utoipa`]: https://docs.rs/utoipa/latest/utoipa
16/// [`utoipa::path`]: https://docs.rs/utoipa/latest/utoipa/attr.path.html
17/// [`utoipa_ts::export!`]: https://docs.rs/utoipa-ts/latest/utoipa_ts/macro.export.html
18///
19/// ## Supported metadata
20///
21/// - HTTP method, e.g. `get`, `post`, `put`
22/// - `path = "/..."`
23/// - `params(...)`
24/// - `request_body = Type`
25/// - `request_body(content = Type, ...)`
26/// - `responses((status = 200, body = Type), ...)`
27#[proc_macro_attribute]
28pub fn path(args: TokenStream, item: TokenStream) -> TokenStream {
29    let original_args = TokenStream2::from(args.clone());
30    let args = parse_macro_input!(args as PathArgs);
31    let input = parse_macro_input!(item as ItemFn);
32
33    expand_path(original_args, args, input).into()
34}
35
36fn expand_path(original_args: TokenStream2, args: PathArgs, input: ItemFn) -> TokenStream2 {
37    let fn_name = &input.sig.ident;
38    let render_name = format_ident!("__utoipa_ts_render_{}", fn_name);
39    let method = args.method.unwrap_or_else(|| "UNKNOWN".to_owned());
40    let path = args.path.unwrap_or_else(|| "/".to_owned());
41
42    let params = args.params.iter().map(|param| {
43        let name = &param.name;
44        let ty = &param.ty;
45        quote! {
46            endpoint.param::<#ty>(#name);
47        }
48    });
49
50    let param_sets = args.param_sets.iter().map(|ty| {
51        quote! {
52            endpoint.params::<#ty>();
53        }
54    });
55
56    let request_body = args.request_body.iter().map(|ty| {
57        quote! {
58            endpoint.request_body::<#ty>();
59        }
60    });
61
62    let responses = args.responses.iter().map(|response| {
63        let status = &response.status;
64        if let Some(ty) = &response.body {
65            quote! {
66                endpoint.response::<#ty>(#status);
67            }
68        } else {
69            quote! {
70                endpoint.empty_response(#status);
71            }
72        }
73    });
74
75    quote! {
76        #[utoipa::path(#original_args)]
77        #input
78
79        fn #render_name(collector: &mut ::utoipa_ts::TypeCollector) -> ::utoipa_ts::EndpointSpec {
80            let mut endpoint = ::utoipa_ts::EndpointRender::new(
81                collector,
82                stringify!(#fn_name),
83                #method,
84                #path,
85            );
86
87            #(#params)*
88            #(#param_sets)*
89            #(#request_body)*
90            #(#responses)*
91
92            endpoint.finish()
93        }
94
95        ::utoipa_ts::__private::inventory::submit! {
96            ::utoipa_ts::Endpoint {
97                name: stringify!(#fn_name),
98                method: #method,
99                path: #path,
100                render: #render_name,
101            }
102        }
103    }
104}
105
106struct PathArgs {
107    method: Option<String>,
108    path: Option<String>,
109    params: Vec<Param>,
110    param_sets: Vec<Type>,
111    request_body: Option<Type>,
112    responses: Vec<Response>,
113}
114
115impl Parse for PathArgs {
116    fn parse(input: ParseStream<'_>) -> Result<Self> {
117        let mut args = Self {
118            method: None,
119            path: None,
120            params: Vec::new(),
121            param_sets: Vec::new(),
122            request_body: None,
123            responses: Vec::new(),
124        };
125
126        while !input.is_empty() {
127            let ident: Ident = input.parse()?;
128            let key = ident.to_string();
129
130            if input.peek(Token![=]) {
131                input.parse::<Token![=]>()?;
132
133                match key.as_str() {
134                    "path" => {
135                        let value: LitStr = input.parse()?;
136                        args.path = Some(value.value());
137                    }
138                    "request_body" => {
139                        args.request_body = Some(input.parse()?);
140                    }
141                    _ => {
142                        let _: Expr = input.parse()?;
143                    }
144                }
145            } else if input.peek(syn::token::Paren) {
146                let content;
147                parenthesized!(content in input);
148
149                match key.as_str() {
150                    "params" => {
151                        let params = parse_params(&content.parse()?)?;
152                        args.params.extend(params.params);
153                        args.param_sets.extend(params.param_sets);
154                    }
155                    "request_body" => args.request_body = parse_request_body(&content.parse()?)?,
156                    "responses" => args.responses.extend(parse_responses(&content.parse()?)?),
157                    _ => {}
158                }
159            } else if args.method.is_none() {
160                args.method = Some(key.to_ascii_uppercase());
161            }
162
163            if input.peek(Token![,]) {
164                input.parse::<Token![,]>()?;
165            }
166        }
167
168        Ok(args)
169    }
170}
171
172struct Param {
173    name: String,
174    ty: Type,
175}
176
177fn parse_params(tokens: &TokenStream2) -> Result<ParamList> {
178    syn::parse2::<ParamList>(tokens.clone())
179}
180
181struct ParamList {
182    params: Vec<Param>,
183    param_sets: Vec<Type>,
184}
185
186impl Parse for ParamList {
187    fn parse(input: ParseStream<'_>) -> Result<Self> {
188        let mut params = Vec::new();
189        let mut param_sets = Vec::new();
190
191        while !input.is_empty() {
192            if input.peek(syn::token::Paren) {
193                let content;
194                parenthesized!(content in input);
195
196                let name: LitStr = content.parse()?;
197                content.parse::<Token![=]>()?;
198                let ty: Type = content.parse()?;
199                params.push(Param {
200                    name: name.value(),
201                    ty,
202                });
203
204                while !content.is_empty() {
205                    let _: proc_macro2::TokenTree = content.parse()?;
206                }
207            } else {
208                param_sets.push(input.parse()?);
209            }
210
211            if input.peek(Token![,]) {
212                input.parse::<Token![,]>()?;
213            }
214        }
215
216        Ok(Self { params, param_sets })
217    }
218}
219
220struct Response {
221    status: String,
222    body: Option<Type>,
223}
224
225fn parse_request_body(tokens: &TokenStream2) -> Result<Option<Type>> {
226    syn::parse2::<RequestBody>(tokens.clone()).map(|body| body.ty)
227}
228
229struct RequestBody {
230    ty: Option<Type>,
231}
232
233impl Parse for RequestBody {
234    fn parse(input: ParseStream<'_>) -> Result<Self> {
235        let mut ty = None;
236
237        while !input.is_empty() {
238            let key: Ident = input.parse()?;
239            input.parse::<Token![=]>()?;
240
241            if key == "content" {
242                ty = Some(input.parse()?);
243            } else {
244                let _ = parse_until_comma(input)?;
245            }
246
247            if input.peek(Token![,]) {
248                input.parse::<Token![,]>()?;
249            }
250        }
251
252        Ok(Self { ty })
253    }
254}
255
256fn parse_responses(tokens: &TokenStream2) -> Result<Vec<Response>> {
257    syn::parse2::<ResponseList>(tokens.clone()).map(|list| list.responses)
258}
259
260struct ResponseList {
261    responses: Vec<Response>,
262}
263
264impl Parse for ResponseList {
265    fn parse(input: ParseStream<'_>) -> Result<Self> {
266        let mut responses = Vec::new();
267
268        while !input.is_empty() {
269            let content;
270            parenthesized!(content in input);
271            responses.push(content.parse()?);
272
273            if input.peek(Token![,]) {
274                input.parse::<Token![,]>()?;
275            }
276        }
277
278        Ok(Self { responses })
279    }
280}
281
282impl Parse for Response {
283    fn parse(input: ParseStream<'_>) -> Result<Self> {
284        let mut status = None;
285        let mut body = None;
286
287        while !input.is_empty() {
288            let key: Ident = input.parse()?;
289            input.parse::<Token![=]>()?;
290
291            match key.to_string().as_str() {
292                "status" => {
293                    let tokens = parse_until_comma(input)?;
294                    status = Some(status_tokens_to_string(tokens));
295                }
296                "body" => {
297                    body = Some(input.parse()?);
298                }
299                _ => {
300                    let _ = parse_until_comma(input)?;
301                }
302            }
303
304            if input.peek(Token![,]) {
305                input.parse::<Token![,]>()?;
306            }
307        }
308
309        Ok(Self {
310            status: status.unwrap_or_else(|| "default".to_owned()),
311            body,
312        })
313    }
314}
315
316fn parse_until_comma(input: ParseStream<'_>) -> Result<TokenStream2> {
317    let mut tokens = TokenStream2::new();
318
319    while !input.is_empty() && !input.peek(Token![,]) {
320        let token: proc_macro2::TokenTree = input.parse()?;
321        tokens.extend([token]);
322    }
323
324    Ok(tokens)
325}
326
327fn status_tokens_to_string(tokens: TokenStream2) -> String {
328    let value = tokens.to_string();
329    value.trim().trim_matches('"').to_owned()
330}