1use proc_macro::TokenStream;
18use quote::quote;
19use syn::{parse_macro_input, ItemFn, LitStr};
20
21fn is_debug_enabled() -> bool {
23 std::env::var("RUSTAPI_DEBUG")
24 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
25 .unwrap_or(false)
26}
27
28fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
30 if is_debug_enabled() {
31 eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
32 eprintln!("{}", tokens);
33 eprintln!("=== END {} ===\n", name);
34 }
35}
36
37fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
41 if !path.starts_with('/') {
43 return Err(syn::Error::new(
44 span,
45 format!("route path must start with '/', got: \"{}\"", path),
46 ));
47 }
48
49 if path.contains("//") {
51 return Err(syn::Error::new(
52 span,
53 format!(
54 "route path contains empty segment (double slash): \"{}\"",
55 path
56 ),
57 ));
58 }
59
60 let mut brace_depth = 0;
62 let mut param_start = None;
63
64 for (i, ch) in path.char_indices() {
65 match ch {
66 '{' => {
67 if brace_depth > 0 {
68 return Err(syn::Error::new(
69 span,
70 format!(
71 "nested braces are not allowed in route path at position {}: \"{}\"",
72 i, path
73 ),
74 ));
75 }
76 brace_depth += 1;
77 param_start = Some(i);
78 }
79 '}' => {
80 if brace_depth == 0 {
81 return Err(syn::Error::new(
82 span,
83 format!(
84 "unmatched closing brace '}}' at position {} in route path: \"{}\"",
85 i, path
86 ),
87 ));
88 }
89 brace_depth -= 1;
90
91 if let Some(start) = param_start {
93 let param_name = &path[start + 1..i];
94 if param_name.is_empty() {
95 return Err(syn::Error::new(
96 span,
97 format!(
98 "empty parameter name '{{}}' at position {} in route path: \"{}\"",
99 start, path
100 ),
101 ));
102 }
103 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
105 return Err(syn::Error::new(
106 span,
107 format!(
108 "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
109 param_name, start, path
110 ),
111 ));
112 }
113 if param_name
115 .chars()
116 .next()
117 .map(|c| c.is_ascii_digit())
118 .unwrap_or(false)
119 {
120 return Err(syn::Error::new(
121 span,
122 format!(
123 "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
124 param_name, start, path
125 ),
126 ));
127 }
128 }
129 param_start = None;
130 }
131 _ if brace_depth == 0 => {
133 if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
135 return Err(syn::Error::new(
136 span,
137 format!(
138 "invalid character '{}' at position {} in route path: \"{}\"",
139 ch, i, path
140 ),
141 ));
142 }
143 }
144 _ => {}
145 }
146 }
147
148 if brace_depth > 0 {
150 return Err(syn::Error::new(
151 span,
152 format!(
153 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
154 path
155 ),
156 ));
157 }
158
159 Ok(())
160}
161
162#[proc_macro_attribute]
180pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
181 let input = parse_macro_input!(item as ItemFn);
182
183 let attrs = &input.attrs;
184 let vis = &input.vis;
185 let sig = &input.sig;
186 let block = &input.block;
187
188 let expanded = quote! {
189 #(#attrs)*
190 #[::tokio::main]
191 #vis #sig {
192 #block
193 }
194 };
195
196 debug_output("main", &expanded);
197
198 TokenStream::from(expanded)
199}
200
201fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
203 let path = parse_macro_input!(attr as LitStr);
204 let input = parse_macro_input!(item as ItemFn);
205
206 let fn_name = &input.sig.ident;
207 let fn_vis = &input.vis;
208 let fn_attrs = &input.attrs;
209 let fn_async = &input.sig.asyncness;
210 let fn_inputs = &input.sig.inputs;
211 let fn_output = &input.sig.output;
212 let fn_block = &input.block;
213 let fn_generics = &input.sig.generics;
214
215 let path_value = path.value();
216
217 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
219 return err.to_compile_error().into();
220 }
221
222 let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
224
225 let route_helper = match method {
227 "GET" => quote!(::rustapi_rs::get_route),
228 "POST" => quote!(::rustapi_rs::post_route),
229 "PUT" => quote!(::rustapi_rs::put_route),
230 "PATCH" => quote!(::rustapi_rs::patch_route),
231 "DELETE" => quote!(::rustapi_rs::delete_route),
232 _ => quote!(::rustapi_rs::get_route),
233 };
234
235 let mut chained_calls = quote!();
237
238 for attr in fn_attrs {
239 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
242 let ident_str = ident.to_string();
243 if ident_str == "tag" {
244 if let Ok(lit) = attr.parse_args::<LitStr>() {
245 let val = lit.value();
246 chained_calls = quote! { #chained_calls .tag(#val) };
247 }
248 } else if ident_str == "summary" {
249 if let Ok(lit) = attr.parse_args::<LitStr>() {
250 let val = lit.value();
251 chained_calls = quote! { #chained_calls .summary(#val) };
252 }
253 } else if ident_str == "description" {
254 if let Ok(lit) = attr.parse_args::<LitStr>() {
255 let val = lit.value();
256 chained_calls = quote! { #chained_calls .description(#val) };
257 }
258 }
259 }
260 }
261
262 let expanded = quote! {
263 #(#fn_attrs)*
265 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
266
267 #[doc(hidden)]
269 #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
270 #route_helper(#path_value, #fn_name)
271 #chained_calls
272 }
273 };
274
275 debug_output(&format!("{} {}", method, path_value), &expanded);
276
277 TokenStream::from(expanded)
278}
279
280#[proc_macro_attribute]
296pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
297 generate_route_handler("GET", attr, item)
298}
299
300#[proc_macro_attribute]
302pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
303 generate_route_handler("POST", attr, item)
304}
305
306#[proc_macro_attribute]
308pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
309 generate_route_handler("PUT", attr, item)
310}
311
312#[proc_macro_attribute]
314pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
315 generate_route_handler("PATCH", attr, item)
316}
317
318#[proc_macro_attribute]
320pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
321 generate_route_handler("DELETE", attr, item)
322}
323
324#[proc_macro_attribute]
340pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
341 let tag = parse_macro_input!(attr as LitStr);
342 let input = parse_macro_input!(item as ItemFn);
343
344 let attrs = &input.attrs;
345 let vis = &input.vis;
346 let sig = &input.sig;
347 let block = &input.block;
348 let tag_value = tag.value();
349
350 let expanded = quote! {
352 #[doc = concat!("**Tag:** ", #tag_value)]
353 #(#attrs)*
354 #vis #sig #block
355 };
356
357 TokenStream::from(expanded)
358}
359
360#[proc_macro_attribute]
372pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
373 let summary = parse_macro_input!(attr as LitStr);
374 let input = parse_macro_input!(item as ItemFn);
375
376 let attrs = &input.attrs;
377 let vis = &input.vis;
378 let sig = &input.sig;
379 let block = &input.block;
380 let summary_value = summary.value();
381
382 let expanded = quote! {
384 #[doc = #summary_value]
385 #(#attrs)*
386 #vis #sig #block
387 };
388
389 TokenStream::from(expanded)
390}
391
392#[proc_macro_attribute]
404pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
405 let desc = parse_macro_input!(attr as LitStr);
406 let input = parse_macro_input!(item as ItemFn);
407
408 let attrs = &input.attrs;
409 let vis = &input.vis;
410 let sig = &input.sig;
411 let block = &input.block;
412 let desc_value = desc.value();
413
414 let expanded = quote! {
416 #[doc = ""]
417 #[doc = #desc_value]
418 #(#attrs)*
419 #vis #sig #block
420 };
421
422 TokenStream::from(expanded)
423}