Skip to main content

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