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!("route path contains empty segment (double slash): \"{}\"", path),
54 ));
55 }
56
57 let mut brace_depth = 0;
59 let mut param_start = None;
60
61 for (i, ch) in path.char_indices() {
62 match ch {
63 '{' => {
64 if brace_depth > 0 {
65 return Err(syn::Error::new(
66 span,
67 format!(
68 "nested braces are not allowed in route path at position {}: \"{}\"",
69 i, path
70 ),
71 ));
72 }
73 brace_depth += 1;
74 param_start = Some(i);
75 }
76 '}' => {
77 if brace_depth == 0 {
78 return Err(syn::Error::new(
79 span,
80 format!(
81 "unmatched closing brace '}}' at position {} in route path: \"{}\"",
82 i, path
83 ),
84 ));
85 }
86 brace_depth -= 1;
87
88 if let Some(start) = param_start {
90 let param_name = &path[start + 1..i];
91 if param_name.is_empty() {
92 return Err(syn::Error::new(
93 span,
94 format!(
95 "empty parameter name '{{}}' at position {} in route path: \"{}\"",
96 start, path
97 ),
98 ));
99 }
100 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
102 return Err(syn::Error::new(
103 span,
104 format!(
105 "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
106 param_name, start, path
107 ),
108 ));
109 }
110 if param_name.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
112 return Err(syn::Error::new(
113 span,
114 format!(
115 "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
116 param_name, start, path
117 ),
118 ));
119 }
120 }
121 param_start = None;
122 }
123 _ if brace_depth == 0 => {
125 if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
127 return Err(syn::Error::new(
128 span,
129 format!(
130 "invalid character '{}' at position {} in route path: \"{}\"",
131 ch, i, path
132 ),
133 ));
134 }
135 }
136 _ => {}
137 }
138 }
139
140 if brace_depth > 0 {
142 return Err(syn::Error::new(
143 span,
144 format!(
145 "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
146 path
147 ),
148 ));
149 }
150
151 Ok(())
152}
153
154#[proc_macro_attribute]
172pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
173 let input = parse_macro_input!(item as ItemFn);
174
175 let attrs = &input.attrs;
176 let vis = &input.vis;
177 let sig = &input.sig;
178 let block = &input.block;
179
180 let expanded = quote! {
181 #(#attrs)*
182 #[::tokio::main]
183 #vis #sig {
184 #block
185 }
186 };
187
188 debug_output("main", &expanded);
189
190 TokenStream::from(expanded)
191}
192
193fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
195 let path = parse_macro_input!(attr as LitStr);
196 let input = parse_macro_input!(item as ItemFn);
197
198 let fn_name = &input.sig.ident;
199 let fn_vis = &input.vis;
200 let fn_attrs = &input.attrs;
201 let fn_async = &input.sig.asyncness;
202 let fn_inputs = &input.sig.inputs;
203 let fn_output = &input.sig.output;
204 let fn_block = &input.block;
205 let fn_generics = &input.sig.generics;
206
207 let path_value = path.value();
208
209 if let Err(err) = validate_path_syntax(&path_value, path.span()) {
211 return err.to_compile_error().into();
212 }
213
214 let route_fn_name = syn::Ident::new(
216 &format!("{}_route", fn_name),
217 fn_name.span()
218 );
219
220 let route_helper = match method {
222 "GET" => quote!(::rustapi_rs::get_route),
223 "POST" => quote!(::rustapi_rs::post_route),
224 "PUT" => quote!(::rustapi_rs::put_route),
225 "PATCH" => quote!(::rustapi_rs::patch_route),
226 "DELETE" => quote!(::rustapi_rs::delete_route),
227 _ => quote!(::rustapi_rs::get_route),
228 };
229
230 let mut chained_calls = quote!();
232
233 for attr in fn_attrs {
234 if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
237 let ident_str = ident.to_string();
238 if ident_str == "tag" {
239 if let Ok(lit) = attr.parse_args::<LitStr>() {
240 let val = lit.value();
241 chained_calls = quote! { #chained_calls .tag(#val) };
242 }
243 } else if ident_str == "summary" {
244 if let Ok(lit) = attr.parse_args::<LitStr>() {
245 let val = lit.value();
246 chained_calls = quote! { #chained_calls .summary(#val) };
247 }
248 } else if ident_str == "description" {
249 if let Ok(lit) = attr.parse_args::<LitStr>() {
250 let val = lit.value();
251 chained_calls = quote! { #chained_calls .description(#val) };
252 }
253 }
254 }
255 }
256
257 let expanded = quote! {
258 #(#fn_attrs)*
260 #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
261
262 #[doc(hidden)]
264 #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
265 #route_helper(#path_value, #fn_name)
266 #chained_calls
267 }
268 };
269
270 debug_output(&format!("{} {}", method, path_value), &expanded);
271
272 TokenStream::from(expanded)
273}
274
275#[proc_macro_attribute]
291pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
292 generate_route_handler("GET", attr, item)
293}
294
295#[proc_macro_attribute]
297pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
298 generate_route_handler("POST", attr, item)
299}
300
301#[proc_macro_attribute]
303pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
304 generate_route_handler("PUT", attr, item)
305}
306
307#[proc_macro_attribute]
309pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
310 generate_route_handler("PATCH", attr, item)
311}
312
313#[proc_macro_attribute]
315pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
316 generate_route_handler("DELETE", attr, item)
317}
318
319#[proc_macro_attribute]
335pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
336 let tag = parse_macro_input!(attr as LitStr);
337 let input = parse_macro_input!(item as ItemFn);
338
339 let attrs = &input.attrs;
340 let vis = &input.vis;
341 let sig = &input.sig;
342 let block = &input.block;
343 let tag_value = tag.value();
344
345 let expanded = quote! {
347 #[doc = concat!("**Tag:** ", #tag_value)]
348 #(#attrs)*
349 #vis #sig #block
350 };
351
352 TokenStream::from(expanded)
353}
354
355#[proc_macro_attribute]
367pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
368 let summary = parse_macro_input!(attr as LitStr);
369 let input = parse_macro_input!(item as ItemFn);
370
371 let attrs = &input.attrs;
372 let vis = &input.vis;
373 let sig = &input.sig;
374 let block = &input.block;
375 let summary_value = summary.value();
376
377 let expanded = quote! {
379 #[doc = #summary_value]
380 #(#attrs)*
381 #vis #sig #block
382 };
383
384 TokenStream::from(expanded)
385}
386
387#[proc_macro_attribute]
399pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
400 let desc = parse_macro_input!(attr as LitStr);
401 let input = parse_macro_input!(item as ItemFn);
402
403 let attrs = &input.attrs;
404 let vis = &input.vis;
405 let sig = &input.sig;
406 let block = &input.block;
407 let desc_value = desc.value();
408
409 let expanded = quote! {
411 #[doc = ""]
412 #[doc = #desc_value]
413 #(#attrs)*
414 #vis #sig #block
415 };
416
417 TokenStream::from(expanded)
418}
419