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#[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 = ¶m.name;
44 let ty = ¶m.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}