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