rustapi_macros/
lib.rs

1//! Procedural macros for RustAPI
2//!
3//! This crate provides the attribute macros used in RustAPI:
4//!
5//! - `#[rustapi::main]` - Main entry point macro
6//! - `#[rustapi::get("/path")]` - GET route handler
7//! - `#[rustapi::post("/path")]` - POST route handler
8//! - `#[rustapi::put("/path")]` - PUT route handler
9//! - `#[rustapi::patch("/path")]` - PATCH route handler
10//! - `#[rustapi::delete("/path")]` - DELETE route handler
11//!
12//! ## Debugging
13//!
14//! Set `RUSTAPI_DEBUG=1` environment variable during compilation to see
15//! expanded macro output for debugging purposes.
16
17use proc_macro::TokenStream;
18use quote::quote;
19use syn::{parse_macro_input, ItemFn, LitStr};
20
21/// Check if RUSTAPI_DEBUG is enabled at compile time
22fn 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
28/// Print debug output if RUSTAPI_DEBUG=1 is set
29fn 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
37/// Validate route path syntax at compile time
38///
39/// Returns Ok(()) if the path is valid, or Err with a descriptive error message.
40fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
41    // Path must start with /
42    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    // Check for empty path segments (double slashes)
50    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    // Validate path parameter syntax
61    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                // Check that parameter name is not empty
92                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                    // Validate parameter name contains only valid identifier characters
104                    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                    // Parameter name must not start with a digit
114                    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            // Check for invalid characters in path (outside of parameters)
132            _ if brace_depth == 0 => {
133                // Allow alphanumeric, -, _, ., /, and common URL characters
134                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    // Check for unclosed braces
149    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/// Main entry point macro for RustAPI applications
163///
164/// This macro wraps your async main function with the tokio runtime.
165///
166/// # Example
167///
168/// ```rust,ignore
169/// use rustapi_rs::prelude::*;
170///
171/// #[rustapi::main]
172/// async fn main() -> Result<()> {
173///     RustApi::new()
174///         .mount(hello)
175///         .run("127.0.0.1:8080")
176///         .await
177/// }
178/// ```
179#[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
201/// Internal helper to generate route handler macros
202fn 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    // Validate path syntax at compile time
218    if let Err(err) = validate_path_syntax(&path_value, path.span()) {
219        return err.to_compile_error().into();
220    }
221
222    // Generate a companion module with route info
223    let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
224
225    // Pick the right route helper function based on method
226    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    // Extract metadata from attributes to chain builder methods
236    let mut chained_calls = quote!();
237
238    for attr in fn_attrs {
239        // Check for tag, summary, description
240        // Use loose matching on the last segment to handle crate renaming or fully qualified paths
241        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        // The original handler function
264        #(#fn_attrs)*
265        #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
266
267        // Route info function - creates a Route for this handler
268        #[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/// GET route handler macro
281///
282/// # Example
283///
284/// ```rust,ignore
285/// #[rustapi::get("/users")]
286/// async fn list_users() -> Json<Vec<User>> {
287///     Json(vec![])
288/// }
289///
290/// #[rustapi::get("/users/{id}")]
291/// async fn get_user(Path(id): Path<i64>) -> Result<User> {
292///     Ok(User { id, name: "John".into() })
293/// }
294/// ```
295#[proc_macro_attribute]
296pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
297    generate_route_handler("GET", attr, item)
298}
299
300/// POST route handler macro
301#[proc_macro_attribute]
302pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
303    generate_route_handler("POST", attr, item)
304}
305
306/// PUT route handler macro
307#[proc_macro_attribute]
308pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
309    generate_route_handler("PUT", attr, item)
310}
311
312/// PATCH route handler macro
313#[proc_macro_attribute]
314pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
315    generate_route_handler("PATCH", attr, item)
316}
317
318/// DELETE route handler macro
319#[proc_macro_attribute]
320pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
321    generate_route_handler("DELETE", attr, item)
322}
323
324// ============================================
325// Route Metadata Macros
326// ============================================
327
328/// Tag macro for grouping endpoints in OpenAPI documentation
329///
330/// # Example
331///
332/// ```rust,ignore
333/// #[rustapi::get("/users")]
334/// #[rustapi::tag("Users")]
335/// async fn list_users() -> Json<Vec<User>> {
336///     Json(vec![])
337/// }
338/// ```
339#[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    // Add a doc comment with the tag info for documentation
351    let expanded = quote! {
352        #[doc = concat!("**Tag:** ", #tag_value)]
353        #(#attrs)*
354        #vis #sig #block
355    };
356
357    TokenStream::from(expanded)
358}
359
360/// Summary macro for endpoint summary in OpenAPI documentation
361///
362/// # Example
363///
364/// ```rust,ignore
365/// #[rustapi::get("/users")]
366/// #[rustapi::summary("List all users")]
367/// async fn list_users() -> Json<Vec<User>> {
368///     Json(vec![])
369/// }
370/// ```
371#[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    // Add a doc comment with the summary
383    let expanded = quote! {
384        #[doc = #summary_value]
385        #(#attrs)*
386        #vis #sig #block
387    };
388
389    TokenStream::from(expanded)
390}
391
392/// Description macro for detailed endpoint description in OpenAPI documentation
393///
394/// # Example
395///
396/// ```rust,ignore
397/// #[rustapi::get("/users")]
398/// #[rustapi::description("Returns a list of all users in the system. Supports pagination.")]
399/// async fn list_users() -> Json<Vec<User>> {
400///     Json(vec![])
401/// }
402/// ```
403#[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    // Add a doc comment with the description
415    let expanded = quote! {
416        #[doc = ""]
417        #[doc = #desc_value]
418        #(#attrs)*
419        #vis #sig #block
420    };
421
422    TokenStream::from(expanded)
423}