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