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!("route path contains empty segment (double slash): \"{}\"", path),
54        ));
55    }
56
57    // Validate path parameter syntax
58    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                // Check that parameter name is not empty
89                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                    // Validate parameter name contains only valid identifier characters
101                    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                    // Parameter name must not start with a digit
111                    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            // Check for invalid characters in path (outside of parameters)
124            _ if brace_depth == 0 => {
125                // Allow alphanumeric, -, _, ., /, and common URL characters
126                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    // Check for unclosed braces
141    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/// Main entry point macro for RustAPI applications
155///
156/// This macro wraps your async main function with the tokio runtime.
157///
158/// # Example
159///
160/// ```rust,ignore
161/// use rustapi_rs::prelude::*;
162///
163/// #[rustapi::main]
164/// async fn main() -> Result<()> {
165///     RustApi::new()
166///         .mount(hello)
167///         .run("127.0.0.1:8080")
168///         .await
169/// }
170/// ```
171#[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
193/// Internal helper to generate route handler macros
194fn 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    // Validate path syntax at compile time
210    if let Err(err) = validate_path_syntax(&path_value, path.span()) {
211        return err.to_compile_error().into();
212    }
213    
214    // Generate a companion module with route info
215    let route_fn_name = syn::Ident::new(
216        &format!("{}_route", fn_name),
217        fn_name.span()
218    );
219    
220    // Pick the right route helper function based on method
221    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    // Extract metadata from attributes to chain builder methods
231    let mut chained_calls = quote!();
232    
233    for attr in fn_attrs {
234        // Check for tag, summary, description
235        // Use loose matching on the last segment to handle crate renaming or fully qualified paths
236        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        // The original handler function
259        #(#fn_attrs)*
260        #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
261        
262        // Route info function - creates a Route for this handler
263        #[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/// GET route handler macro
276///
277/// # Example
278///
279/// ```rust,ignore
280/// #[rustapi::get("/users")]
281/// async fn list_users() -> Json<Vec<User>> {
282///     Json(vec![])
283/// }
284///
285/// #[rustapi::get("/users/{id}")]
286/// async fn get_user(Path(id): Path<i64>) -> Result<User> {
287///     Ok(User { id, name: "John".into() })
288/// }
289/// ```
290#[proc_macro_attribute]
291pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
292    generate_route_handler("GET", attr, item)
293}
294
295/// POST route handler macro
296#[proc_macro_attribute]
297pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
298    generate_route_handler("POST", attr, item)
299}
300
301/// PUT route handler macro
302#[proc_macro_attribute]
303pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
304    generate_route_handler("PUT", attr, item)
305}
306
307/// PATCH route handler macro
308#[proc_macro_attribute]
309pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
310    generate_route_handler("PATCH", attr, item)
311}
312
313/// DELETE route handler macro
314#[proc_macro_attribute]
315pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
316    generate_route_handler("DELETE", attr, item)
317}
318
319// ============================================
320// Route Metadata Macros
321// ============================================
322
323/// Tag macro for grouping endpoints in OpenAPI documentation
324///
325/// # Example
326///
327/// ```rust,ignore
328/// #[rustapi::get("/users")]
329/// #[rustapi::tag("Users")]
330/// async fn list_users() -> Json<Vec<User>> {
331///     Json(vec![])
332/// }
333/// ```
334#[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    // Add a doc comment with the tag info for documentation
346    let expanded = quote! {
347        #[doc = concat!("**Tag:** ", #tag_value)]
348        #(#attrs)*
349        #vis #sig #block
350    };
351    
352    TokenStream::from(expanded)
353}
354
355/// Summary macro for endpoint summary in OpenAPI documentation
356///
357/// # Example
358///
359/// ```rust,ignore
360/// #[rustapi::get("/users")]
361/// #[rustapi::summary("List all users")]
362/// async fn list_users() -> Json<Vec<User>> {
363///     Json(vec![])
364/// }
365/// ```
366#[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    // Add a doc comment with the summary
378    let expanded = quote! {
379        #[doc = #summary_value]
380        #(#attrs)*
381        #vis #sig #block
382    };
383    
384    TokenStream::from(expanded)
385}
386
387/// Description macro for detailed endpoint description in OpenAPI documentation
388///
389/// # Example
390///
391/// ```rust,ignore
392/// #[rustapi::get("/users")]
393/// #[rustapi::description("Returns a list of all users in the system. Supports pagination.")]
394/// async fn list_users() -> Json<Vec<User>> {
395///     Json(vec![])
396/// }
397/// ```
398#[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    // Add a doc comment with the description
410    let expanded = quote! {
411        #[doc = ""]
412        #[doc = #desc_value]
413        #(#attrs)*
414        #vis #sig #block
415    };
416    
417    TokenStream::from(expanded)
418}
419