1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4 parse::{Parse, ParseStream},
5 punctuated::Punctuated,
6 Expr, FnArg, GenericArgument, ItemFn, LitBool, LitStr, Meta, Pat, PathArguments, ReturnType,
7 Token, Type,
8};
9
10#[proc_macro_attribute]
12pub fn api_get(attr: TokenStream, item: TokenStream) -> TokenStream {
13 expand("get", attr, item)
14}
15
16#[proc_macro_attribute]
18pub fn api_post(attr: TokenStream, item: TokenStream) -> TokenStream {
19 expand("post", attr, item)
20}
21
22fn expand(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
23 let args = syn::parse_macro_input!(attr as ApiArgs);
24 let mut func = syn::parse_macro_input!(item as ItemFn);
25
26 if args.openapi == Some(false) {
27 let dioxus_attr = dioxus_route_attr(method, &args.path);
28 return quote! {
29 #dioxus_attr
30 #func
31 }
32 .into();
33 }
34
35 let tag = match &args.tag {
36 Some(t) => t.clone(),
37 None => {
38 return syn::Error::new_spanned(
39 &func.sig.ident,
40 "api_get/api_post require `tag = \"...\"` (or `openapi = false`)",
41 )
42 .to_compile_error()
43 .into();
44 }
45 };
46
47 let path_lit = &args.path;
48 let path_value = path_lit.value();
49 let fn_ident = func.sig.ident.clone();
50 let openapi_fn = format_ident!("__openapi_{}", fn_ident);
51 let body_struct = format_ident!("__openapi_body_{}", fn_ident);
52 let operation_id = fn_ident.to_string();
53
54 let path_params = parse_path_params(&path_value);
55 let arg_map = fn_arg_map(&func);
56
57 let mut param_tokens = Vec::new();
59 for name in &path_params {
60 let Some((_, (ty, _))) = arg_map.iter().find(|(n, _)| n == name) else {
61 return syn::Error::new_spanned(
62 &func.sig.ident,
63 format!("path parameter `{{{name}}}` has no matching function argument"),
64 )
65 .to_compile_error()
66 .into();
67 };
68 let name_lit = LitStr::new(name, proc_macro2::Span::call_site());
69 param_tokens.push(quote! {
70 (#name_lit = #ty, Path, description = #name_lit)
71 });
72 }
73
74 let body_fields: Vec<_> = arg_map
76 .iter()
77 .filter(|(name, _)| !path_params.iter().any(|p| p == name))
78 .map(|(name, (ty, _))| {
79 let ident = format_ident!("{}", name);
80 (ident, ty.clone())
81 })
82 .collect();
83
84 let has_body = method == "post" && !body_fields.is_empty() && args.request_body.is_none();
85 let request_body_override = args.request_body.clone();
86
87 let response_ty = match &args.response {
88 Some(ty) => Some(ty.clone()),
89 None => match extract_result_ok(&func.sig.output) {
90 Ok(ty) => ty,
91 Err(e) => return e.to_compile_error().into(),
92 },
93 };
94
95 let dioxus_attr = dioxus_route_attr(method, path_lit);
96 let method_ident = format_ident!("{}", method);
97
98 let docs: Vec<_> = func
101 .attrs
102 .iter()
103 .filter(|a| a.path().is_ident("doc"))
104 .cloned()
105 .collect();
106
107 let _ = &mut func;
109
110 let body_struct_tokens = if has_body {
111 let fields = body_fields.iter().map(|(ident, ty)| {
112 quote! { pub #ident: #ty }
113 });
114 quote! {
115 #[cfg(feature = "server")]
116 #[derive(utoipa::ToSchema)]
117 #[allow(non_camel_case_types)]
118 #[doc(hidden)]
119 pub struct #body_struct {
120 #(#fields,)*
121 }
122 }
123 } else {
124 quote! {}
125 };
126
127 let request_body_tokens = if let Some(ty) = request_body_override {
128 quote! { request_body = #ty, }
129 } else if has_body {
130 quote! { request_body = #body_struct, }
131 } else {
132 quote! {}
133 };
134
135 let params_tokens = if param_tokens.is_empty() {
136 quote! {}
137 } else {
138 quote! { params( #(#param_tokens),* ), }
139 };
140
141 let responses_tokens = match response_ty {
142 None => quote! {
143 responses(
144 (status = 200, description = "OK"),
145 (status = 500, description = "Server error")
146 )
147 },
148 Some(Type::Tuple(t)) if t.elems.is_empty() => quote! {
149 responses(
150 (status = 200, description = "OK"),
151 (status = 500, description = "Server error")
152 )
153 },
154 Some(ty) => quote! {
155 responses(
156 (status = 200, description = "OK", body = #ty),
157 (status = 500, description = "Server error")
158 )
159 },
160 };
161
162 let path_type = format_ident!("__path_{}", openapi_fn);
164
165 quote! {
166 #dioxus_attr
167 #func
168
169 #body_struct_tokens
170
171 #[cfg(feature = "server")]
172 #(#docs)*
173 #[utoipa::path(
174 #method_ident,
175 path = #path_lit,
176 operation_id = #operation_id,
177 tag = #tag,
178 #params_tokens
179 #request_body_tokens
180 #responses_tokens
181 )]
182 #[doc(hidden)]
183 pub fn #openapi_fn() {}
184
185 #[cfg(feature = "server")]
189 ::dioxus_openapi::inventory::submit! {
190 ::dioxus_openapi::RegisteredPath {
191 append: |paths| ::dioxus_openapi::append_tagged_path::<#path_type>(paths),
192 schemas: |schemas| {
193 <#path_type as ::utoipa::__dev::SchemaReferences>::schemas(schemas);
194 },
195 }
196 }
197 }
198 .into()
199}
200
201fn dioxus_route_attr(method: &str, path: &LitStr) -> proc_macro2::TokenStream {
202 match method {
203 "get" => quote! { #[::dioxus::prelude::get(#path)] },
204 "post" => quote! { #[::dioxus::prelude::post(#path)] },
205 "put" => quote! { #[::dioxus::prelude::put(#path)] },
206 "delete" => quote! { #[::dioxus::prelude::delete(#path)] },
207 "patch" => quote! { #[::dioxus::prelude::patch(#path)] },
208 other => {
209 let msg = format!("unsupported method {other}");
210 quote! { compile_error!(#msg) }
211 }
212 }
213}
214
215struct ApiArgs {
216 path: LitStr,
217 tag: Option<LitStr>,
218 request_body: Option<Type>,
219 response: Option<Type>,
220 openapi: Option<bool>,
221}
222
223impl Parse for ApiArgs {
224 fn parse(input: ParseStream) -> syn::Result<Self> {
225 let path: LitStr = input.parse()?;
226 let mut tag = None;
227 let mut request_body = None;
228 let mut response = None;
229 let mut openapi = None;
230
231 if input.peek(Token![,]) {
232 let _ = input.parse::<Token![,]>()?;
233 let punct: Punctuated<Meta, Token![,]> = Punctuated::parse_terminated(input)?;
234 for meta in punct {
235 match meta {
236 Meta::NameValue(nv) if nv.path.is_ident("tag") => {
237 tag = Some(expr_to_lit_str(&nv.value)?);
238 }
239 Meta::NameValue(nv) if nv.path.is_ident("request_body") => {
240 request_body = Some(expr_to_type(&nv.value)?);
241 }
242 Meta::NameValue(nv) if nv.path.is_ident("response") => {
243 response = Some(expr_to_type(&nv.value)?);
244 }
245 Meta::NameValue(nv) if nv.path.is_ident("openapi") => {
246 openapi = Some(expr_to_bool(&nv.value)?);
247 }
248 other => {
249 return Err(syn::Error::new_spanned(
250 other,
251 "expected tag / request_body / response / openapi",
252 ));
253 }
254 }
255 }
256 }
257
258 Ok(Self {
259 path,
260 tag,
261 request_body,
262 response,
263 openapi,
264 })
265 }
266}
267
268fn expr_to_lit_str(expr: &Expr) -> syn::Result<LitStr> {
269 match expr {
270 Expr::Lit(syn::ExprLit {
271 lit: syn::Lit::Str(s),
272 ..
273 }) => Ok(s.clone()),
274 _ => Err(syn::Error::new_spanned(expr, "expected string literal")),
275 }
276}
277
278fn expr_to_bool(expr: &Expr) -> syn::Result<bool> {
279 match expr {
280 Expr::Lit(syn::ExprLit {
281 lit: syn::Lit::Bool(LitBool { value, .. }),
282 ..
283 }) => Ok(*value),
284 _ => Err(syn::Error::new_spanned(expr, "expected bool literal")),
285 }
286}
287
288fn expr_to_type(expr: &Expr) -> syn::Result<Type> {
289 syn::parse2(quote! { #expr })
292}
293
294fn parse_path_params(path: &str) -> Vec<String> {
296 let mut out = Vec::new();
297 let mut rest = path;
298 while let Some(start) = rest.find('{') {
299 rest = &rest[start + 1..];
300 let Some(end) = rest.find('}') else { break };
301 let name = &rest[..end];
302 let name = name.strip_prefix('*').unwrap_or(name);
304 if !name.is_empty() {
305 out.push(name.to_string());
306 }
307 rest = &rest[end + 1..];
308 }
309 out
310}
311
312fn fn_arg_map(func: &ItemFn) -> Vec<(String, (Type, usize))> {
314 let mut out = Vec::new();
316 for (i, arg) in func.sig.inputs.iter().enumerate() {
317 let FnArg::Typed(pat_ty) = arg else {
318 continue;
319 };
320 let Pat::Ident(pat_ident) = pat_ty.pat.as_ref() else {
321 continue;
322 };
323 out.push((
324 pat_ident.ident.to_string(),
325 ((*pat_ty.ty).clone(), i),
326 ));
327 }
328 out
329}
330
331fn extract_result_ok(ret: &ReturnType) -> syn::Result<Option<Type>> {
333 let ReturnType::Type(_, ty) = ret else {
334 return Ok(None);
335 };
336 let Type::Path(type_path) = ty.as_ref() else {
337 return Ok(Some(ty.as_ref().clone()));
338 };
339 let last = type_path
340 .path
341 .segments
342 .last()
343 .ok_or_else(|| syn::Error::new_spanned(ty, "empty return type path"))?;
344 if last.ident != "Result" {
345 return Ok(Some(ty.as_ref().clone()));
346 }
347 let PathArguments::AngleBracketed(args) = &last.arguments else {
348 return Ok(None);
349 };
350 let mut generics = args.args.iter().filter_map(|a| match a {
351 GenericArgument::Type(t) => Some(t.clone()),
352 _ => None,
353 });
354 Ok(generics.next())
355}