rustapi_macros/
lib.rs

1//!
2//! This crate provides the attribute macros used in RustAPI:
3//!
4//! - `#[rustapi::main]` - Main entry point macro
5//! - `#[rustapi::get("/path")]` - GET route handler
6//! - `#[rustapi::post("/path")]` - POST route handler
7//! - `#[rustapi::put("/path")]` - PUT route handler
8//! - `#[rustapi::patch("/path")]` - PATCH route handler
9//! - `#[rustapi::delete("/path")]` - DELETE route handler
10//! - `#[derive(Validate)]` - Validation derive macro
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 std::collections::HashSet;
20use syn::{
21    parse_macro_input, Attribute, Data, DeriveInput, Expr, Fields, FnArg, GenericArgument, ItemFn,
22    Lit, LitStr, Meta, PathArguments, ReturnType, Type,
23};
24
25mod api_error;
26
27/// Auto-register a schema type for zero-config OpenAPI.
28///
29/// Attach this to a `struct` or `enum` that also derives `Schema` (utoipa::ToSchema).
30/// This ensures the type is registered into RustAPI's OpenAPI components even if it is
31/// only referenced indirectly (e.g. as a nested field type).
32///
33/// ```rust,ignore
34/// use rustapi_rs::prelude::*;
35///
36/// #[rustapi_rs::schema]
37/// #[derive(Serialize, Schema)]
38/// struct UserInfo { /* ... */ }
39/// ```
40#[proc_macro_attribute]
41pub fn schema(_attr: TokenStream, item: TokenStream) -> TokenStream {
42    let input = parse_macro_input!(item as syn::Item);
43
44    let (ident, generics) = match &input {
45        syn::Item::Struct(s) => (&s.ident, &s.generics),
46        syn::Item::Enum(e) => (&e.ident, &e.generics),
47        _ => {
48            return syn::Error::new_spanned(
49                &input,
50                "#[rustapi_rs::schema] can only be used on structs or enums",
51            )
52            .to_compile_error()
53            .into();
54        }
55    };
56
57    if !generics.params.is_empty() {
58        return syn::Error::new_spanned(
59            generics,
60            "#[rustapi_rs::schema] does not support generic types",
61        )
62        .to_compile_error()
63        .into();
64    }
65
66    let registrar_ident = syn::Ident::new(
67        &format!("__RUSTAPI_AUTO_SCHEMA_{}", ident),
68        proc_macro2::Span::call_site(),
69    );
70
71    let expanded = quote! {
72        #input
73
74        #[allow(non_upper_case_globals)]
75        #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
76        #[linkme(crate = ::rustapi_rs::__private::linkme)]
77        static #registrar_ident: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) =
78            |spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec| {
79                spec.register_in_place::<#ident>();
80            };
81    };
82
83    debug_output("schema", &expanded);
84    expanded.into()
85}
86
87fn extract_schema_types(ty: &Type, out: &mut Vec<Type>, allow_leaf: bool) {
88    match ty {
89        Type::Reference(r) => extract_schema_types(&r.elem, out, allow_leaf),
90        Type::Path(tp) => {
91            let Some(seg) = tp.path.segments.last() else {
92                return;
93            };
94
95            let ident = seg.ident.to_string();
96
97            let unwrap_first_generic = |out: &mut Vec<Type>| {
98                if let PathArguments::AngleBracketed(args) = &seg.arguments {
99                    if let Some(GenericArgument::Type(inner)) = args.args.first() {
100                        extract_schema_types(inner, out, true);
101                    }
102                }
103            };
104
105            match ident.as_str() {
106                // Request/response wrappers
107                "Json" | "ValidatedJson" | "Created" => {
108                    unwrap_first_generic(out);
109                }
110                // WithStatus<T, CODE>
111                "WithStatus" => {
112                    if let PathArguments::AngleBracketed(args) = &seg.arguments {
113                        if let Some(GenericArgument::Type(inner)) = args.args.first() {
114                            extract_schema_types(inner, out, true);
115                        }
116                    }
117                }
118                // Common combinators
119                "Option" | "Result" => {
120                    if let PathArguments::AngleBracketed(args) = &seg.arguments {
121                        if let Some(GenericArgument::Type(inner)) = args.args.first() {
122                            extract_schema_types(inner, out, allow_leaf);
123                        }
124                    }
125                }
126                _ => {
127                    if allow_leaf {
128                        out.push(ty.clone());
129                    }
130                }
131            }
132        }
133        _ => {}
134    }
135}
136
137fn collect_handler_schema_types(input: &ItemFn) -> Vec<Type> {
138    let mut found: Vec<Type> = Vec::new();
139
140    for arg in &input.sig.inputs {
141        if let FnArg::Typed(pat_ty) = arg {
142            extract_schema_types(&pat_ty.ty, &mut found, false);
143        }
144    }
145
146    if let ReturnType::Type(_, ty) = &input.sig.output {
147        extract_schema_types(ty, &mut found, false);
148    }
149
150    // Dedup by token string.
151    let mut seen = HashSet::<String>::new();
152    found
153        .into_iter()
154        .filter(|t| seen.insert(quote!(#t).to_string()))
155        .collect()
156}
157
158/// Extract path parameter names from a route path string.
159/// e.g., "/users/{user_id}/posts/{post_id}" -> vec!["user_id", "post_id"]
160fn extract_path_params_from_route(path: &str) -> Vec<String> {
161    let mut params = Vec::new();
162    let mut in_brace = false;
163    let mut current = String::new();
164
165    for ch in path.chars() {
166        match ch {
167            '{' => {
168                in_brace = true;
169                current.clear();
170            }
171            '}' => {
172                if in_brace && !current.is_empty() {
173                    params.push(current.clone());
174                }
175                in_brace = false;
176                current.clear();
177            }
178            _ if in_brace => {
179                current.push(ch);
180            }
181            _ => {}
182        }
183    }
184    params
185}
186
187/// Extract parameter name from a pattern like `Path(id)` or `Path((user_id, post_id))`.
188/// Returns a list of extracted names in order.
189fn extract_param_names_from_pattern(pat: &syn::Pat) -> Vec<String> {
190    let mut names = Vec::new();
191
192    match pat {
193        // Path(id) style - single param destructuring
194        syn::Pat::TupleStruct(ts) => {
195            for elem in &ts.elems {
196                match elem {
197                    // Single ident: Path(id)
198                    syn::Pat::Ident(pi) => {
199                        names.push(pi.ident.to_string());
200                    }
201                    // Tuple inside: Path((user_id, post_id))
202                    syn::Pat::Tuple(tuple) => {
203                        for inner in &tuple.elems {
204                            if let syn::Pat::Ident(pi) = inner {
205                                names.push(pi.ident.to_string());
206                            }
207                        }
208                    }
209                    _ => {}
210                }
211            }
212        }
213        // path: Path<T> style (no destructuring)
214        syn::Pat::Ident(pi) => {
215            names.push(pi.ident.to_string());
216        }
217        _ => {}
218    }
219    names
220}
221
222/// Extract inner types from a Path<T> or Path<(A, B, ...)> type.
223/// Returns a list of type strings in order.
224fn extract_inner_types_from_path(ty: &Type) -> Vec<String> {
225    let mut types = Vec::new();
226
227    if let Type::Path(type_path) = ty {
228        if let Some(seg) = type_path.path.segments.last() {
229            if seg.ident == "Path" {
230                if let PathArguments::AngleBracketed(args) = &seg.arguments {
231                    if let Some(GenericArgument::Type(inner)) = args.args.first() {
232                        // Check if it's a tuple type
233                        if let Type::Tuple(tuple) = inner {
234                            for elem in &tuple.elems {
235                                types.push(quote!(#elem).to_string().replace(' ', ""));
236                            }
237                        } else {
238                            // Single type
239                            types.push(quote!(#inner).to_string().replace(' ', ""));
240                        }
241                    }
242                }
243            }
244        }
245    }
246    types
247}
248
249/// Extract Path<T> types from handler function arguments.
250/// Returns a vector of (param_name, rust_type) pairs.
251/// For tuple paths like Path<(Uuid, i64)>, the route_path is used to match param names.
252fn extract_path_param_types(input: &ItemFn, route_path: &str) -> Vec<(String, String)> {
253    let route_params = extract_path_params_from_route(route_path);
254    let mut result = Vec::new();
255
256    for arg in &input.sig.inputs {
257        if let FnArg::Typed(pat_ty) = arg {
258            // Check if type is Path<T>
259            if let Type::Path(type_path) = &*pat_ty.ty {
260                if let Some(seg) = type_path.path.segments.last() {
261                    if seg.ident == "Path" {
262                        let inner_types = extract_inner_types_from_path(&pat_ty.ty);
263                        let param_names = extract_param_names_from_pattern(&pat_ty.pat);
264
265                        // If we have multiple types (tuple), match with route params
266                        if inner_types.len() > 1 {
267                            // Use route_params order to match types
268                            for (i, param_name) in route_params.iter().enumerate() {
269                                if let Some(type_str) = inner_types.get(i) {
270                                    result.push((param_name.clone(), type_str.clone()));
271                                }
272                            }
273                        } else if let Some(type_str) = inner_types.first() {
274                            // Single param - use extracted name or first route param
275                            let name = param_names
276                                .first()
277                                .cloned()
278                                .or_else(|| route_params.first().cloned())
279                                .unwrap_or_else(|| "id".to_string());
280                            result.push((name, type_str.clone()));
281                        }
282                    }
283                }
284            }
285        }
286    }
287    result
288}
289
290/// Map Rust type to OpenAPI schema type string.
291fn rust_type_to_schema_type(type_str: &str) -> &'static str {
292    // Normalize the type string (remove whitespace, handle fully qualified paths)
293    let normalized = type_str
294        .replace(' ', "")
295        .replace("::", "")
296        .to_lowercase();
297
298    // Check for common type patterns
299    if normalized.contains("uuid") {
300        return "uuid";
301    }
302
303    match normalized.as_str() {
304        "i64" | "u64" | "isize" | "usize" => "int64",
305        "i32" | "u32" => "int32",
306        "i16" | "u16" | "i8" | "u8" => "int32",
307        "f64" | "f32" => "number",
308        "bool" => "boolean",
309        "string" | "str" | "&str" => "string",
310        _ => "string", // Default to string for unknown types
311    }
312}
313
314/// Check if RUSTAPI_DEBUG is enabled at compile time
315fn is_debug_enabled() -> bool {
316    std::env::var("RUSTAPI_DEBUG")
317        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
318        .unwrap_or(false)
319}
320
321/// Print debug output if RUSTAPI_DEBUG=1 is set
322fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
323    if is_debug_enabled() {
324        eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
325        eprintln!("{}", tokens);
326        eprintln!("=== END {} ===\n", name);
327    }
328}
329
330/// Validate route path syntax at compile time
331///
332/// Returns Ok(()) if the path is valid, or Err with a descriptive error message.
333fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
334    // Path must start with /
335    if !path.starts_with('/') {
336        return Err(syn::Error::new(
337            span,
338            format!("route path must start with '/', got: \"{}\"", path),
339        ));
340    }
341
342    // Check for empty path segments (double slashes)
343    if path.contains("//") {
344        return Err(syn::Error::new(
345            span,
346            format!(
347                "route path contains empty segment (double slash): \"{}\"",
348                path
349            ),
350        ));
351    }
352
353    // Validate path parameter syntax
354    let mut brace_depth = 0;
355    let mut param_start = None;
356
357    for (i, ch) in path.char_indices() {
358        match ch {
359            '{' => {
360                if brace_depth > 0 {
361                    return Err(syn::Error::new(
362                        span,
363                        format!(
364                            "nested braces are not allowed in route path at position {}: \"{}\"",
365                            i, path
366                        ),
367                    ));
368                }
369                brace_depth += 1;
370                param_start = Some(i);
371            }
372            '}' => {
373                if brace_depth == 0 {
374                    return Err(syn::Error::new(
375                        span,
376                        format!(
377                            "unmatched closing brace '}}' at position {} in route path: \"{}\"",
378                            i, path
379                        ),
380                    ));
381                }
382                brace_depth -= 1;
383
384                // Check that parameter name is not empty
385                if let Some(start) = param_start {
386                    let param_name = &path[start + 1..i];
387                    if param_name.is_empty() {
388                        return Err(syn::Error::new(
389                            span,
390                            format!(
391                                "empty parameter name '{{}}' at position {} in route path: \"{}\"",
392                                start, path
393                            ),
394                        ));
395                    }
396                    // Validate parameter name contains only valid identifier characters
397                    if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
398                        return Err(syn::Error::new(
399                            span,
400                            format!(
401                                "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
402                                param_name, start, path
403                            ),
404                        ));
405                    }
406                    // Parameter name must not start with a digit
407                    if param_name
408                        .chars()
409                        .next()
410                        .map(|c| c.is_ascii_digit())
411                        .unwrap_or(false)
412                    {
413                        return Err(syn::Error::new(
414                            span,
415                            format!(
416                                "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
417                                param_name, start, path
418                            ),
419                        ));
420                    }
421                }
422                param_start = None;
423            }
424            // Check for invalid characters in path (outside of parameters)
425            _ if brace_depth == 0 => {
426                // Allow alphanumeric, -, _, ., /, and common URL characters
427                if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
428                    return Err(syn::Error::new(
429                        span,
430                        format!(
431                            "invalid character '{}' at position {} in route path: \"{}\"",
432                            ch, i, path
433                        ),
434                    ));
435                }
436            }
437            _ => {}
438        }
439    }
440
441    // Check for unclosed braces
442    if brace_depth > 0 {
443        return Err(syn::Error::new(
444            span,
445            format!(
446                "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
447                path
448            ),
449        ));
450    }
451
452    Ok(())
453}
454
455/// Main entry point macro for RustAPI applications
456///
457/// This macro wraps your async main function with the tokio runtime.
458///
459/// # Example
460///
461/// ```rust,ignore
462/// use rustapi_rs::prelude::*;
463///
464/// #[rustapi::main]
465/// async fn main() -> Result<()> {
466///     RustApi::new()
467///         .mount(hello)
468///         .run("127.0.0.1:8080")
469///         .await
470/// }
471/// ```
472#[proc_macro_attribute]
473pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
474    let input = parse_macro_input!(item as ItemFn);
475
476    let attrs = &input.attrs;
477    let vis = &input.vis;
478    let sig = &input.sig;
479    let block = &input.block;
480
481    let expanded = quote! {
482        #(#attrs)*
483        #[::tokio::main]
484        #vis #sig {
485            #block
486        }
487    };
488
489    debug_output("main", &expanded);
490
491    TokenStream::from(expanded)
492}
493
494/// Internal helper to generate route handler macros
495fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
496    let path = parse_macro_input!(attr as LitStr);
497    let input = parse_macro_input!(item as ItemFn);
498
499    let fn_name = &input.sig.ident;
500    let fn_vis = &input.vis;
501    let fn_attrs = &input.attrs;
502    let fn_async = &input.sig.asyncness;
503    let fn_inputs = &input.sig.inputs;
504    let fn_output = &input.sig.output;
505    let fn_block = &input.block;
506    let fn_generics = &input.sig.generics;
507
508    let schema_types = collect_handler_schema_types(&input);
509
510    let path_value = path.value();
511
512    // Validate path syntax at compile time
513    if let Err(err) = validate_path_syntax(&path_value, path.span()) {
514        return err.to_compile_error().into();
515    }
516
517    // Generate a companion module with route info
518    let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
519    // Generate unique name for auto-registration static
520    let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
521
522    // Generate unique names for schema registration
523    let schema_reg_fn_name =
524        syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
525    let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
526
527    // Pick the right route helper function based on method
528    let route_helper = match method {
529        "GET" => quote!(::rustapi_rs::get_route),
530        "POST" => quote!(::rustapi_rs::post_route),
531        "PUT" => quote!(::rustapi_rs::put_route),
532        "PATCH" => quote!(::rustapi_rs::patch_route),
533        "DELETE" => quote!(::rustapi_rs::delete_route),
534        _ => quote!(::rustapi_rs::get_route),
535    };
536
537    // Extract metadata from attributes to chain builder methods
538    let mut chained_calls = quote!();
539
540    // Collect manually specified param schemas (to allow override)
541    let mut manual_params = HashSet::<String>::new();
542
543    // First pass: collect manually specified #[param] attributes
544    for attr in fn_attrs {
545        if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
546            if ident == "param" {
547                if let Ok(param_args) = attr.parse_args_with(
548                    syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
549                ) {
550                    for meta in &param_args {
551                        match meta {
552                            Meta::Path(path) => {
553                                if let Some(ident) = path.get_ident() {
554                                    manual_params.insert(ident.to_string());
555                                }
556                            }
557                            Meta::NameValue(nv) => {
558                                let key = nv.path.get_ident().map(|i| i.to_string());
559                                if let Some(key) = key {
560                                    if key != "schema" && key != "type" {
561                                        manual_params.insert(key);
562                                    }
563                                }
564                            }
565                            _ => {}
566                        }
567                    }
568                }
569            }
570        }
571    }
572
573    // Auto-detect Path<T> types from handler arguments and add .param() calls
574    // Only for params NOT manually specified via #[param]
575    let auto_param_types = extract_path_param_types(&input, &path_value);
576    for (param_name, rust_type) in &auto_param_types {
577        if !manual_params.contains(param_name) {
578            let schema_type = rust_type_to_schema_type(rust_type);
579            chained_calls = quote! { #chained_calls .param(#param_name, #schema_type) };
580        }
581    }
582
583    for attr in fn_attrs {
584        // Check for tag, summary, description, param
585        // Use loose matching on the last segment to handle crate renaming or fully qualified paths
586        if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
587            let ident_str = ident.to_string();
588            if ident_str == "tag" {
589                if let Ok(lit) = attr.parse_args::<LitStr>() {
590                    let val = lit.value();
591                    chained_calls = quote! { #chained_calls .tag(#val) };
592                }
593            } else if ident_str == "summary" {
594                if let Ok(lit) = attr.parse_args::<LitStr>() {
595                    let val = lit.value();
596                    chained_calls = quote! { #chained_calls .summary(#val) };
597                }
598            } else if ident_str == "description" {
599                if let Ok(lit) = attr.parse_args::<LitStr>() {
600                    let val = lit.value();
601                    chained_calls = quote! { #chained_calls .description(#val) };
602                }
603            } else if ident_str == "param" {
604                // Parse #[param(name, schema = "type")] or #[param(name = "type")]
605                if let Ok(param_args) = attr.parse_args_with(
606                    syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
607                ) {
608                    let mut param_name: Option<String> = None;
609                    let mut param_schema: Option<String> = None;
610
611                    for meta in param_args {
612                        match &meta {
613                            // Simple ident: #[param(id, ...)]
614                            Meta::Path(path) => {
615                                if param_name.is_none() {
616                                    if let Some(ident) = path.get_ident() {
617                                        param_name = Some(ident.to_string());
618                                    }
619                                }
620                            }
621                            // Named value: #[param(schema = "uuid")] or #[param(id = "uuid")]
622                            Meta::NameValue(nv) => {
623                                let key = nv.path.get_ident().map(|i| i.to_string());
624                                if let Some(key) = key {
625                                    if key == "schema" || key == "type" {
626                                        if let Expr::Lit(lit) = &nv.value {
627                                            if let Lit::Str(s) = &lit.lit {
628                                                param_schema = Some(s.value());
629                                            }
630                                        }
631                                    } else if param_name.is_none() {
632                                        // Treat as #[param(name = "schema")]
633                                        param_name = Some(key);
634                                        if let Expr::Lit(lit) = &nv.value {
635                                            if let Lit::Str(s) = &lit.lit {
636                                                param_schema = Some(s.value());
637                                            }
638                                        }
639                                    }
640                                }
641                            }
642                            _ => {}
643                        }
644                    }
645
646                    if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
647                        chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
648                    }
649                }
650            }
651        }
652    }
653
654    let expanded = quote! {
655        // The original handler function
656        #(#fn_attrs)*
657        #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
658
659        // Route info function - creates a Route for this handler
660        #[doc(hidden)]
661        #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
662            #route_helper(#path_value, #fn_name)
663                #chained_calls
664        }
665
666        // Auto-register route with linkme
667        #[doc(hidden)]
668        #[allow(non_upper_case_globals)]
669        #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_ROUTES)]
670        #[linkme(crate = ::rustapi_rs::__private::linkme)]
671        static #auto_route_name: fn() -> ::rustapi_rs::Route = #route_fn_name;
672
673        // Auto-register referenced schemas with linkme (best-effort)
674        #[doc(hidden)]
675        #[allow(non_snake_case)]
676        fn #schema_reg_fn_name(spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) {
677            #( spec.register_in_place::<#schema_types>(); )*
678        }
679
680        #[doc(hidden)]
681        #[allow(non_upper_case_globals)]
682        #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
683        #[linkme(crate = ::rustapi_rs::__private::linkme)]
684        static #auto_schema_name: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) = #schema_reg_fn_name;
685    };
686
687    debug_output(&format!("{} {}", method, path_value), &expanded);
688
689    TokenStream::from(expanded)
690}
691
692/// GET route handler macro
693///
694/// # Example
695///
696/// ```rust,ignore
697/// #[rustapi::get("/users")]
698/// async fn list_users() -> Json<Vec<User>> {
699///     Json(vec![])
700/// }
701///
702/// #[rustapi::get("/users/{id}")]
703/// async fn get_user(Path(id): Path<i64>) -> Result<User> {
704///     Ok(User { id, name: "John".into() })
705/// }
706/// ```
707#[proc_macro_attribute]
708pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
709    generate_route_handler("GET", attr, item)
710}
711
712/// POST route handler macro
713#[proc_macro_attribute]
714pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
715    generate_route_handler("POST", attr, item)
716}
717
718/// PUT route handler macro
719#[proc_macro_attribute]
720pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
721    generate_route_handler("PUT", attr, item)
722}
723
724/// PATCH route handler macro
725#[proc_macro_attribute]
726pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
727    generate_route_handler("PATCH", attr, item)
728}
729
730/// DELETE route handler macro
731#[proc_macro_attribute]
732pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
733    generate_route_handler("DELETE", attr, item)
734}
735
736// ============================================
737// Route Metadata Macros
738// ============================================
739
740/// Tag macro for grouping endpoints in OpenAPI documentation
741///
742/// # Example
743///
744/// ```rust,ignore
745/// #[rustapi::get("/users")]
746/// #[rustapi::tag("Users")]
747/// async fn list_users() -> Json<Vec<User>> {
748///     Json(vec![])
749/// }
750/// ```
751#[proc_macro_attribute]
752pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
753    let tag = parse_macro_input!(attr as LitStr);
754    let input = parse_macro_input!(item as ItemFn);
755
756    let attrs = &input.attrs;
757    let vis = &input.vis;
758    let sig = &input.sig;
759    let block = &input.block;
760    let tag_value = tag.value();
761
762    // Add a doc comment with the tag info for documentation
763    let expanded = quote! {
764        #[doc = concat!("**Tag:** ", #tag_value)]
765        #(#attrs)*
766        #vis #sig #block
767    };
768
769    TokenStream::from(expanded)
770}
771
772/// Summary macro for endpoint summary in OpenAPI documentation
773///
774/// # Example
775///
776/// ```rust,ignore
777/// #[rustapi::get("/users")]
778/// #[rustapi::summary("List all users")]
779/// async fn list_users() -> Json<Vec<User>> {
780///     Json(vec![])
781/// }
782/// ```
783#[proc_macro_attribute]
784pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
785    let summary = parse_macro_input!(attr as LitStr);
786    let input = parse_macro_input!(item as ItemFn);
787
788    let attrs = &input.attrs;
789    let vis = &input.vis;
790    let sig = &input.sig;
791    let block = &input.block;
792    let summary_value = summary.value();
793
794    // Add a doc comment with the summary
795    let expanded = quote! {
796        #[doc = #summary_value]
797        #(#attrs)*
798        #vis #sig #block
799    };
800
801    TokenStream::from(expanded)
802}
803
804/// Description macro for detailed endpoint description in OpenAPI documentation
805///
806/// # Example
807///
808/// ```rust,ignore
809/// #[rustapi::get("/users")]
810/// #[rustapi::description("Returns a list of all users in the system. Supports pagination.")]
811/// async fn list_users() -> Json<Vec<User>> {
812///     Json(vec![])
813/// }
814/// ```
815#[proc_macro_attribute]
816pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
817    let desc = parse_macro_input!(attr as LitStr);
818    let input = parse_macro_input!(item as ItemFn);
819
820    let attrs = &input.attrs;
821    let vis = &input.vis;
822    let sig = &input.sig;
823    let block = &input.block;
824    let desc_value = desc.value();
825
826    // Add a doc comment with the description
827    let expanded = quote! {
828        #[doc = ""]
829        #[doc = #desc_value]
830        #(#attrs)*
831        #vis #sig #block
832    };
833
834    TokenStream::from(expanded)
835}
836
837/// Path parameter schema macro for OpenAPI documentation
838///
839/// Use this to specify the OpenAPI schema type for a path parameter when
840/// the auto-inferred type is incorrect. This is particularly useful for
841/// UUID parameters that might be named `id`.
842///
843/// # Supported schema types
844/// - `"uuid"` - String with UUID format
845/// - `"integer"` or `"int"` - Integer with int64 format
846/// - `"string"` - Plain string
847/// - `"boolean"` or `"bool"` - Boolean
848/// - `"number"` - Number (float)
849///
850/// # Example
851///
852/// ```rust,ignore
853/// use uuid::Uuid;
854///
855/// #[rustapi::get("/users/{id}")]
856/// #[rustapi::param(id, schema = "uuid")]
857/// async fn get_user(Path(id): Path<Uuid>) -> Json<User> {
858///     // ...
859/// }
860///
861/// // Alternative syntax:
862/// #[rustapi::get("/posts/{post_id}")]
863/// #[rustapi::param(post_id = "uuid")]
864/// async fn get_post(Path(post_id): Path<Uuid>) -> Json<Post> {
865///     // ...
866/// }
867/// ```
868#[proc_macro_attribute]
869pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
870    // The param attribute is processed by the route macro (get, post, etc.)
871    // This macro just passes through the function unchanged
872    item
873}
874
875// ============================================
876// Validation Derive Macro
877// ============================================
878
879/// Parsed validation rule from field attributes
880#[derive(Debug)]
881struct ValidationRuleInfo {
882    rule_type: String,
883    params: Vec<(String, String)>,
884    message: Option<String>,
885    groups: Vec<String>,
886}
887
888/// Parse validation attributes from a field
889fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
890    let mut rules = Vec::new();
891
892    for attr in attrs {
893        if !attr.path().is_ident("validate") {
894            continue;
895        }
896
897        // Parse the validate attribute
898        if let Ok(meta) = attr.parse_args::<Meta>() {
899            if let Some(rule) = parse_validate_meta(&meta) {
900                rules.push(rule);
901            }
902        } else if let Ok(nested) = attr
903            .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
904        {
905            for meta in nested {
906                if let Some(rule) = parse_validate_meta(&meta) {
907                    rules.push(rule);
908                }
909            }
910        }
911    }
912
913    rules
914}
915
916/// Parse a single validation meta item
917fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
918    match meta {
919        Meta::Path(path) => {
920            // Simple rule like #[validate(email)]
921            let ident = path.get_ident()?.to_string();
922            Some(ValidationRuleInfo {
923                rule_type: ident,
924                params: Vec::new(),
925                message: None,
926                groups: Vec::new(),
927            })
928        }
929        Meta::List(list) => {
930            // Rule with params like #[validate(length(min = 3, max = 50))]
931            let rule_type = list.path.get_ident()?.to_string();
932            let mut params = Vec::new();
933            let mut message = None;
934            let mut groups = Vec::new();
935
936            // Parse nested params
937            if let Ok(nested) = list.parse_args_with(
938                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
939            ) {
940                for nested_meta in nested {
941                    if let Meta::NameValue(nv) = &nested_meta {
942                        let key = nv.path.get_ident()?.to_string();
943
944                        if key == "groups" {
945                            let vec = expr_to_string_vec(&nv.value);
946                            groups.extend(vec);
947                        } else if let Some(value) = expr_to_string(&nv.value) {
948                            if key == "message" {
949                                message = Some(value);
950                            } else if key == "group" {
951                                groups.push(value);
952                            } else {
953                                params.push((key, value));
954                            }
955                        }
956                    } else if let Meta::Path(path) = &nested_meta {
957                        // Handle flags like #[validate(ip(v4))]
958                        if let Some(ident) = path.get_ident() {
959                            params.push((ident.to_string(), "true".to_string()));
960                        }
961                    }
962                }
963            }
964
965            Some(ValidationRuleInfo {
966                rule_type,
967                params,
968                message,
969                groups,
970            })
971        }
972        Meta::NameValue(nv) => {
973            // Rule like #[validate(regex = "pattern")]
974            let rule_type = nv.path.get_ident()?.to_string();
975            let value = expr_to_string(&nv.value)?;
976
977            Some(ValidationRuleInfo {
978                rule_type: rule_type.clone(),
979                params: vec![(rule_type.clone(), value)],
980                message: None,
981                groups: Vec::new(),
982            })
983        }
984    }
985}
986
987/// Convert an expression to a string value
988fn expr_to_string(expr: &Expr) -> Option<String> {
989    match expr {
990        Expr::Lit(lit) => match &lit.lit {
991            Lit::Str(s) => Some(s.value()),
992            Lit::Int(i) => Some(i.base10_digits().to_string()),
993            Lit::Float(f) => Some(f.base10_digits().to_string()),
994            Lit::Bool(b) => Some(b.value.to_string()),
995            _ => None,
996        },
997        _ => None,
998    }
999}
1000
1001/// Convert an expression to a vector of strings
1002fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
1003    match expr {
1004        Expr::Array(arr) => {
1005            let mut result = Vec::new();
1006            for elem in &arr.elems {
1007                if let Some(s) = expr_to_string(elem) {
1008                    result.push(s);
1009                }
1010            }
1011            result
1012        }
1013        _ => {
1014            if let Some(s) = expr_to_string(expr) {
1015                vec![s]
1016            } else {
1017                Vec::new()
1018            }
1019        }
1020    }
1021}
1022
1023fn generate_rule_validation(
1024    field_name: &str,
1025    _field_type: &Type,
1026    rule: &ValidationRuleInfo,
1027) -> proc_macro2::TokenStream {
1028    let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1029    let field_name_str = field_name;
1030
1031    // Generate group check
1032    let group_check = if rule.groups.is_empty() {
1033        quote! { true }
1034    } else {
1035        let group_names = rule.groups.iter().map(|g| g.as_str());
1036        quote! {
1037            {
1038                let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
1039                rule_groups.iter().any(|g| g.matches(&group))
1040            }
1041        }
1042    };
1043
1044    let validation_logic = match rule.rule_type.as_str() {
1045        "email" => {
1046            let message = rule
1047                .message
1048                .as_ref()
1049                .map(|m| quote! { .with_message(#m) })
1050                .unwrap_or_default();
1051            quote! {
1052                {
1053                    let rule = ::rustapi_validate::v2::EmailRule::new() #message;
1054                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1055                        errors.add(#field_name_str, e);
1056                    }
1057                }
1058            }
1059        }
1060        "length" => {
1061            let min = rule
1062                .params
1063                .iter()
1064                .find(|(k, _)| k == "min")
1065                .and_then(|(_, v)| v.parse::<usize>().ok());
1066            let max = rule
1067                .params
1068                .iter()
1069                .find(|(k, _)| k == "max")
1070                .and_then(|(_, v)| v.parse::<usize>().ok());
1071            let message = rule
1072                .message
1073                .as_ref()
1074                .map(|m| quote! { .with_message(#m) })
1075                .unwrap_or_default();
1076
1077            let rule_creation = match (min, max) {
1078                (Some(min), Some(max)) => {
1079                    quote! { ::rustapi_validate::v2::LengthRule::new(#min, #max) }
1080                }
1081                (Some(min), None) => quote! { ::rustapi_validate::v2::LengthRule::min(#min) },
1082                (None, Some(max)) => quote! { ::rustapi_validate::v2::LengthRule::max(#max) },
1083                (None, None) => quote! { ::rustapi_validate::v2::LengthRule::new(0, usize::MAX) },
1084            };
1085
1086            quote! {
1087                {
1088                    let rule = #rule_creation #message;
1089                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1090                        errors.add(#field_name_str, e);
1091                    }
1092                }
1093            }
1094        }
1095        "range" => {
1096            let min = rule
1097                .params
1098                .iter()
1099                .find(|(k, _)| k == "min")
1100                .map(|(_, v)| v.clone());
1101            let max = rule
1102                .params
1103                .iter()
1104                .find(|(k, _)| k == "max")
1105                .map(|(_, v)| v.clone());
1106            let message = rule
1107                .message
1108                .as_ref()
1109                .map(|m| quote! { .with_message(#m) })
1110                .unwrap_or_default();
1111
1112            // Determine the numeric type from the field type
1113            let rule_creation = match (min, max) {
1114                (Some(min), Some(max)) => {
1115                    let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1116                    let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1117                    quote! { ::rustapi_validate::v2::RangeRule::new(#min_lit, #max_lit) }
1118                }
1119                (Some(min), None) => {
1120                    let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1121                    quote! { ::rustapi_validate::v2::RangeRule::min(#min_lit) }
1122                }
1123                (None, Some(max)) => {
1124                    let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1125                    quote! { ::rustapi_validate::v2::RangeRule::max(#max_lit) }
1126                }
1127                (None, None) => {
1128                    return quote! {};
1129                }
1130            };
1131
1132            quote! {
1133                {
1134                    let rule = #rule_creation #message;
1135                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1136                        errors.add(#field_name_str, e);
1137                    }
1138                }
1139            }
1140        }
1141        "regex" => {
1142            let pattern = rule
1143                .params
1144                .iter()
1145                .find(|(k, _)| k == "regex" || k == "pattern")
1146                .map(|(_, v)| v.clone())
1147                .unwrap_or_default();
1148            let message = rule
1149                .message
1150                .as_ref()
1151                .map(|m| quote! { .with_message(#m) })
1152                .unwrap_or_default();
1153
1154            quote! {
1155                {
1156                    let rule = ::rustapi_validate::v2::RegexRule::new(#pattern) #message;
1157                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1158                        errors.add(#field_name_str, e);
1159                    }
1160                }
1161            }
1162        }
1163        "url" => {
1164            let message = rule
1165                .message
1166                .as_ref()
1167                .map(|m| quote! { .with_message(#m) })
1168                .unwrap_or_default();
1169            quote! {
1170                {
1171                    let rule = ::rustapi_validate::v2::UrlRule::new() #message;
1172                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1173                        errors.add(#field_name_str, e);
1174                    }
1175                }
1176            }
1177        }
1178        "required" => {
1179            let message = rule
1180                .message
1181                .as_ref()
1182                .map(|m| quote! { .with_message(#m) })
1183                .unwrap_or_default();
1184            quote! {
1185                {
1186                    let rule = ::rustapi_validate::v2::RequiredRule::new() #message;
1187                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1188                        errors.add(#field_name_str, e);
1189                    }
1190                }
1191            }
1192        }
1193        "credit_card" => {
1194            let message = rule
1195                .message
1196                .as_ref()
1197                .map(|m| quote! { .with_message(#m) })
1198                .unwrap_or_default();
1199            quote! {
1200                {
1201                    let rule = ::rustapi_validate::v2::CreditCardRule::new() #message;
1202                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1203                        errors.add(#field_name_str, e);
1204                    }
1205                }
1206            }
1207        }
1208        "ip" => {
1209            let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1210            let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1211
1212            let rule_creation = if v4 && !v6 {
1213                quote! { ::rustapi_validate::v2::IpRule::v4() }
1214            } else if !v4 && v6 {
1215                quote! { ::rustapi_validate::v2::IpRule::v6() }
1216            } else {
1217                quote! { ::rustapi_validate::v2::IpRule::new() }
1218            };
1219
1220            let message = rule
1221                .message
1222                .as_ref()
1223                .map(|m| quote! { .with_message(#m) })
1224                .unwrap_or_default();
1225
1226            quote! {
1227                {
1228                    let rule = #rule_creation #message;
1229                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1230                        errors.add(#field_name_str, e);
1231                    }
1232                }
1233            }
1234        }
1235        "phone" => {
1236            let message = rule
1237                .message
1238                .as_ref()
1239                .map(|m| quote! { .with_message(#m) })
1240                .unwrap_or_default();
1241            quote! {
1242                {
1243                    let rule = ::rustapi_validate::v2::PhoneRule::new() #message;
1244                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1245                        errors.add(#field_name_str, e);
1246                    }
1247                }
1248            }
1249        }
1250        "contains" => {
1251            let needle = rule
1252                .params
1253                .iter()
1254                .find(|(k, _)| k == "needle")
1255                .map(|(_, v)| v.clone())
1256                .unwrap_or_default();
1257
1258            let message = rule
1259                .message
1260                .as_ref()
1261                .map(|m| quote! { .with_message(#m) })
1262                .unwrap_or_default();
1263
1264            quote! {
1265                {
1266                    let rule = ::rustapi_validate::v2::ContainsRule::new(#needle) #message;
1267                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1268                        errors.add(#field_name_str, e);
1269                    }
1270                }
1271            }
1272        }
1273        _ => {
1274            // Unknown rule - skip
1275            quote! {}
1276        }
1277    };
1278
1279    quote! {
1280        if #group_check {
1281            #validation_logic
1282        }
1283    }
1284}
1285
1286/// Generate async validation code for a single rule
1287fn generate_async_rule_validation(
1288    field_name: &str,
1289    rule: &ValidationRuleInfo,
1290) -> proc_macro2::TokenStream {
1291    let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1292    let field_name_str = field_name;
1293
1294    // Generate group check
1295    let group_check = if rule.groups.is_empty() {
1296        quote! { true }
1297    } else {
1298        let group_names = rule.groups.iter().map(|g| g.as_str());
1299        quote! {
1300            {
1301                let rule_groups = [#(::rustapi_validate::v2::ValidationGroup::from(#group_names)),*];
1302                rule_groups.iter().any(|g| g.matches(&group))
1303            }
1304        }
1305    };
1306
1307    let validation_logic = match rule.rule_type.as_str() {
1308        "async_unique" => {
1309            let table = rule
1310                .params
1311                .iter()
1312                .find(|(k, _)| k == "table")
1313                .map(|(_, v)| v.clone())
1314                .unwrap_or_default();
1315            let column = rule
1316                .params
1317                .iter()
1318                .find(|(k, _)| k == "column")
1319                .map(|(_, v)| v.clone())
1320                .unwrap_or_default();
1321            let message = rule
1322                .message
1323                .as_ref()
1324                .map(|m| quote! { .with_message(#m) })
1325                .unwrap_or_default();
1326
1327            quote! {
1328                {
1329                    let rule = ::rustapi_validate::v2::AsyncUniqueRule::new(#table, #column) #message;
1330                    if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1331                        errors.add(#field_name_str, e);
1332                    }
1333                }
1334            }
1335        }
1336        "async_exists" => {
1337            let table = rule
1338                .params
1339                .iter()
1340                .find(|(k, _)| k == "table")
1341                .map(|(_, v)| v.clone())
1342                .unwrap_or_default();
1343            let column = rule
1344                .params
1345                .iter()
1346                .find(|(k, _)| k == "column")
1347                .map(|(_, v)| v.clone())
1348                .unwrap_or_default();
1349            let message = rule
1350                .message
1351                .as_ref()
1352                .map(|m| quote! { .with_message(#m) })
1353                .unwrap_or_default();
1354
1355            quote! {
1356                {
1357                    let rule = ::rustapi_validate::v2::AsyncExistsRule::new(#table, #column) #message;
1358                    if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1359                        errors.add(#field_name_str, e);
1360                    }
1361                }
1362            }
1363        }
1364        "async_api" => {
1365            let endpoint = rule
1366                .params
1367                .iter()
1368                .find(|(k, _)| k == "endpoint")
1369                .map(|(_, v)| v.clone())
1370                .unwrap_or_default();
1371            let message = rule
1372                .message
1373                .as_ref()
1374                .map(|m| quote! { .with_message(#m) })
1375                .unwrap_or_default();
1376
1377            quote! {
1378                {
1379                    let rule = ::rustapi_validate::v2::AsyncApiRule::new(#endpoint) #message;
1380                    if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1381                        errors.add(#field_name_str, e);
1382                    }
1383                }
1384            }
1385        }
1386        "custom_async" => {
1387            // #[validate(custom_async = "function_path")]
1388            let function_path = rule
1389                .params
1390                .iter()
1391                .find(|(k, _)| k == "custom_async" || k == "function")
1392                .map(|(_, v)| v.clone())
1393                .unwrap_or_default();
1394
1395            if function_path.is_empty() {
1396                // If path is missing, don't generate invalid code
1397                quote! {}
1398            } else {
1399                let func: syn::Path = syn::parse_str(&function_path).unwrap();
1400                let message_handling = if let Some(msg) = &rule.message {
1401                    quote! {
1402                        let e = ::rustapi_validate::v2::RuleError::new("custom_async", #msg);
1403                        errors.add(#field_name_str, e);
1404                    }
1405                } else {
1406                    quote! {
1407                        errors.add(#field_name_str, e);
1408                    }
1409                };
1410
1411                quote! {
1412                    {
1413                        // Call the custom async function: async fn(&T, &ValidationContext) -> Result<(), RuleError>
1414                        if let Err(e) = #func(&self.#field_ident, ctx).await {
1415                            #message_handling
1416                        }
1417                    }
1418                }
1419            }
1420        }
1421        _ => {
1422            // Not an async rule
1423            quote! {}
1424        }
1425    };
1426
1427    quote! {
1428        if #group_check {
1429            #validation_logic
1430        }
1431    }
1432}
1433
1434/// Check if a rule is async
1435fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1436    matches!(
1437        rule.rule_type.as_str(),
1438        "async_unique" | "async_exists" | "async_api" | "custom_async"
1439    )
1440}
1441
1442/// Derive macro for implementing Validate and AsyncValidate traits
1443///
1444/// # Example
1445///
1446/// ```rust,ignore
1447/// use rustapi_macros::Validate;
1448///
1449/// #[derive(Validate)]
1450/// struct CreateUser {
1451///     #[validate(email, message = "Invalid email format")]
1452///     email: String,
1453///     
1454///     #[validate(length(min = 3, max = 50))]
1455///     username: String,
1456///     
1457///     #[validate(range(min = 18, max = 120))]
1458///     age: u8,
1459///     
1460///     #[validate(async_unique(table = "users", column = "email"))]
1461///     email: String,
1462/// }
1463/// ```
1464#[proc_macro_derive(Validate, attributes(validate))]
1465pub fn derive_validate(input: TokenStream) -> TokenStream {
1466    let input = parse_macro_input!(input as DeriveInput);
1467    let name = &input.ident;
1468    let generics = &input.generics;
1469    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1470
1471    // Only support structs with named fields
1472    let fields = match &input.data {
1473        Data::Struct(data) => match &data.fields {
1474            Fields::Named(fields) => &fields.named,
1475            _ => {
1476                return syn::Error::new_spanned(
1477                    &input,
1478                    "Validate can only be derived for structs with named fields",
1479                )
1480                .to_compile_error()
1481                .into();
1482            }
1483        },
1484        _ => {
1485            return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1486                .to_compile_error()
1487                .into();
1488        }
1489    };
1490
1491    // Collect sync and async validation code for each field
1492    let mut sync_validations = Vec::new();
1493    let mut async_validations = Vec::new();
1494    let mut has_async_rules = false;
1495
1496    for field in fields {
1497        let field_name = field.ident.as_ref().unwrap().to_string();
1498        let field_type = &field.ty;
1499        let rules = parse_validate_attrs(&field.attrs);
1500
1501        for rule in &rules {
1502            if is_async_rule(rule) {
1503                has_async_rules = true;
1504                let validation = generate_async_rule_validation(&field_name, rule);
1505                async_validations.push(validation);
1506            } else {
1507                let validation = generate_rule_validation(&field_name, field_type, rule);
1508                sync_validations.push(validation);
1509            }
1510        }
1511    }
1512
1513    // Generate the Validate impl
1514    let validate_impl = quote! {
1515        impl #impl_generics ::rustapi_validate::v2::Validate for #name #ty_generics #where_clause {
1516            fn validate_with_group(&self, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1517                let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1518
1519                #(#sync_validations)*
1520
1521                errors.into_result()
1522            }
1523        }
1524    };
1525
1526    // Generate the AsyncValidate impl if there are async rules
1527    let async_validate_impl = if has_async_rules {
1528        quote! {
1529            #[::async_trait::async_trait]
1530            impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1531                async fn validate_async_with_group(&self, ctx: &::rustapi_validate::v2::ValidationContext, group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1532                    let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1533
1534                    #(#async_validations)*
1535
1536                    errors.into_result()
1537                }
1538            }
1539        }
1540    } else {
1541        // Provide a default AsyncValidate impl that just returns Ok
1542        quote! {
1543            #[::async_trait::async_trait]
1544            impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1545                async fn validate_async_with_group(&self, _ctx: &::rustapi_validate::v2::ValidationContext, _group: ::rustapi_validate::v2::ValidationGroup) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1546                    Ok(())
1547                }
1548            }
1549        }
1550    };
1551
1552    let expanded = quote! {
1553        #validate_impl
1554        #async_validate_impl
1555    };
1556
1557    debug_output("Validate derive", &expanded);
1558
1559    TokenStream::from(expanded)
1560}
1561
1562// ============================================
1563// ApiError Derive Macro
1564// ============================================
1565
1566/// Derive macro for implementing IntoResponse for error enums
1567///
1568/// # Example
1569///
1570/// ```rust,ignore
1571/// #[derive(ApiError)]
1572/// enum UserError {
1573///     #[error(status = 404, message = "User not found")]
1574///     NotFound(i64),
1575///     
1576///     #[error(status = 400, code = "validation_error")]
1577///     InvalidInput(String),
1578/// }
1579/// ```
1580#[proc_macro_derive(ApiError, attributes(error))]
1581pub fn derive_api_error(input: TokenStream) -> TokenStream {
1582    api_error::expand_derive_api_error(input)
1583}
1584
1585// ============================================
1586// TypedPath Derive Macro
1587// ============================================
1588
1589/// Derive macro for TypedPath
1590///
1591/// # Example
1592///
1593/// ```rust,ignore
1594/// #[derive(TypedPath, Deserialize, Serialize)]
1595/// #[typed_path("/users/{id}/posts/{post_id}")]
1596/// struct PostPath {
1597///     id: u64,
1598///     post_id: String,
1599/// }
1600/// ```
1601#[proc_macro_derive(TypedPath, attributes(typed_path))]
1602pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1603    let input = parse_macro_input!(input as DeriveInput);
1604    let name = &input.ident;
1605    let generics = &input.generics;
1606    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1607
1608    // Find the #[typed_path("...")] attribute
1609    let mut path_str = None;
1610    for attr in &input.attrs {
1611        if attr.path().is_ident("typed_path") {
1612            if let Ok(lit) = attr.parse_args::<LitStr>() {
1613                path_str = Some(lit.value());
1614            }
1615        }
1616    }
1617
1618    let path = match path_str {
1619        Some(p) => p,
1620        None => {
1621            return syn::Error::new_spanned(
1622                &input,
1623                "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1624            )
1625            .to_compile_error()
1626            .into();
1627        }
1628    };
1629
1630    // Validate path syntax
1631    if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1632        return err.to_compile_error().into();
1633    }
1634
1635    // Generate to_uri implementation
1636    // We need to parse the path and replace {param} with self.param
1637    let mut format_string = String::new();
1638    let mut format_args = Vec::new();
1639
1640    let mut chars = path.chars().peekable();
1641    while let Some(ch) = chars.next() {
1642        if ch == '{' {
1643            let mut param_name = String::new();
1644            while let Some(&c) = chars.peek() {
1645                if c == '}' {
1646                    chars.next(); // Consume '}'
1647                    break;
1648                }
1649                param_name.push(chars.next().unwrap());
1650            }
1651
1652            if param_name.is_empty() {
1653                return syn::Error::new_spanned(
1654                    &input,
1655                    "Empty path parameter not allowed in typed_path",
1656                )
1657                .to_compile_error()
1658                .into();
1659            }
1660
1661            format_string.push_str("{}");
1662            let ident = syn::Ident::new(&param_name, proc_macro2::Span::call_site());
1663            format_args.push(quote! { self.#ident });
1664        } else {
1665            format_string.push(ch);
1666        }
1667    }
1668
1669    let expanded = quote! {
1670        impl #impl_generics ::rustapi_rs::prelude::TypedPath for #name #ty_generics #where_clause {
1671            const PATH: &'static str = #path;
1672
1673            fn to_uri(&self) -> String {
1674                format!(#format_string, #(#format_args),*)
1675            }
1676        }
1677    };
1678
1679    debug_output("TypedPath derive", &expanded);
1680    TokenStream::from(expanded)
1681}