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 == "mcp" {
655                // Rich #[mcp(...)] support.
656                // We build a rustapi_openapi::McpOperation struct and call .mcp(meta)
657                if let Ok(mcp_args) = attr.parse_args_with(
658                    syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
659                ) {
660                    let mut skip = quote! { None };
661                    let mut readonly = quote! { None };
662                    let mut write = quote! { None };
663                    let mut require = quote! { None };
664
665                    for meta in mcp_args {
666                        match &meta {
667                            Meta::Path(path) => {
668                                if let Some(ident) = path.get_ident() {
669                                    let s = ident.to_string().to_lowercase();
670                                    if s == "skip" {
671                                        skip = quote! { Some(true) };
672                                    } else if s == "readonly" {
673                                        readonly = quote! { Some(true) };
674                                    } else if s == "write" {
675                                        write = quote! { Some(true) };
676                                    }
677                                }
678                            }
679                            Meta::NameValue(nv) => {
680                                let key = nv.path.get_ident().map(|i| i.to_string().to_lowercase());
681                                if key.as_deref() == Some("require") {
682                                    if let Expr::Lit(lit) = &nv.value {
683                                        if let Lit::Str(s) = &lit.lit {
684                                            let val = s.value();
685                                            require = quote! { Some(#val.to_string()) };
686                                        }
687                                    }
688                                }
689                            }
690                            _ => {}
691                        }
692                    }
693
694                    chained_calls = quote! {
695                        #chained_calls .mcp( #rustapi_path::__private::openapi::McpOperation {
696                            skip: #skip,
697                            readonly: #readonly,
698                            write: #write,
699                            require: #require,
700                        })
701                    };
702                }
703            } else if ident_str == "param" {
704                // Parse #[param(name, schema = "type")] or #[param(name = "type")]
705                if let Ok(param_args) = attr.parse_args_with(
706                    syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
707                ) {
708                    let mut param_name: Option<String> = None;
709                    let mut param_schema: Option<String> = None;
710
711                    for meta in param_args {
712                        match &meta {
713                            // Simple ident: #[param(id, ...)]
714                            Meta::Path(path) if param_name.is_none() => {
715                                if let Some(ident) = path.get_ident() {
716                                    param_name = Some(ident.to_string());
717                                }
718                            }
719                            // Named value: #[param(schema = "uuid")] or #[param(id = "uuid")]
720                            Meta::NameValue(nv) => {
721                                let key = nv.path.get_ident().map(|i| i.to_string());
722                                if let Some(key) = key {
723                                    if key == "schema" || key == "type" {
724                                        if let Expr::Lit(lit) = &nv.value {
725                                            if let Lit::Str(s) = &lit.lit {
726                                                param_schema = Some(s.value());
727                                            }
728                                        }
729                                    } else if param_name.is_none() {
730                                        // Treat as #[param(name = "schema")]
731                                        param_name = Some(key);
732                                        if let Expr::Lit(lit) = &nv.value {
733                                            if let Lit::Str(s) = &lit.lit {
734                                                param_schema = Some(s.value());
735                                            }
736                                        }
737                                    }
738                                }
739                            }
740                            _ => {}
741                        }
742                    }
743
744                    if let (Some(pname), Some(pschema)) = (param_name, param_schema) {
745                        chained_calls = quote! { #chained_calls .param(#pname, #pschema) };
746                    }
747                }
748            } else if ident_str == "errors" {
749                // Parse #[errors(404 = "Not Found", 403 = "Forbidden")]
750                if let Ok(error_args) = attr.parse_args_with(
751                    syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
752                ) {
753                    for meta in error_args {
754                        if let Meta::NameValue(nv) = &meta {
755                            // The path is the status code (e.g., 404)
756                            // We need to parse it as an integer from the ident
757                            let status_str = nv.path.get_ident().map(|i| i.to_string());
758                            if let Some(status_key) = status_str {
759                                // Status code may be a name like `not_found` or number-prefixed
760                                if let Expr::Lit(lit) = &nv.value {
761                                    if let Lit::Str(s) = &lit.lit {
762                                        let desc = s.value();
763                                        chained_calls = quote! {
764                                            #chained_calls .error_response(#status_key, #desc)
765                                        };
766                                    }
767                                }
768                            }
769                        } else if let Meta::List(list) = &meta {
770                            // Handle #[errors(404(description = "Not Found"))]
771                            // For now, skip complex forms
772                            let _ = list;
773                        }
774                    }
775                }
776                // Also try parsing as direct key-value with integer keys:
777                // #[errors(404 = "Not Found")] - integers can't be Meta idents
778                // So we parse the raw token stream manually
779                if let Ok(ts) = attr.parse_args::<proc_macro2::TokenStream>() {
780                    let tokens: Vec<proc_macro2::TokenTree> = ts.into_iter().collect();
781                    let mut i = 0;
782                    while i < tokens.len() {
783                        // Look for pattern: INTEGER = "string" [,]
784                        if let proc_macro2::TokenTree::Literal(lit) = &tokens[i] {
785                            let lit_str = lit.to_string();
786                            if let Ok(status_code) = lit_str.parse::<u16>() {
787                                // Next should be '='
788                                if i + 2 < tokens.len() {
789                                    if let proc_macro2::TokenTree::Punct(p) = &tokens[i + 1] {
790                                        if p.as_char() == '=' {
791                                            if let proc_macro2::TokenTree::Literal(desc_lit) =
792                                                &tokens[i + 2]
793                                            {
794                                                let desc_str = desc_lit.to_string();
795                                                // Remove surrounding quotes
796                                                let desc = desc_str.trim_matches('"').to_string();
797                                                chained_calls = quote! {
798                                                    #chained_calls .error_response(#status_code, #desc)
799                                                };
800                                                i += 3;
801                                                // Skip comma
802                                                if i < tokens.len() {
803                                                    if let proc_macro2::TokenTree::Punct(p) =
804                                                        &tokens[i]
805                                                    {
806                                                        if p.as_char() == ',' {
807                                                            i += 1;
808                                                        }
809                                                    }
810                                                }
811                                                continue;
812                                            }
813                                        }
814                                    }
815                                }
816                            }
817                        }
818                        i += 1;
819                    }
820                }
821            }
822        }
823    }
824
825    let expanded = quote! {
826        // The original handler function
827        #(#fn_attrs)*
828        #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
829
830        // Route info function - creates a Route for this handler
831        #[doc(hidden)]
832        #fn_vis fn #route_fn_name() -> #rustapi_path::Route {
833            #route_helper(#path_value, #fn_name)
834                #chained_calls
835        }
836
837        // Auto-register this route factory using linkme distributed slices.
838        // The `#[linkme(crate = ...)]` attribute is required for correct
839        // operation when the user renames the `rustapi-rs` crate.
840        #[doc(hidden)]
841        #[allow(non_upper_case_globals)]
842        #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_ROUTES)]
843        #[linkme(crate = #rustapi_path::__private::linkme)]
844        static #auto_route_name: fn() -> #rustapi_path::Route = #route_fn_name;
845
846        // Auto-register referenced schemas with linkme (best-effort)
847        #[doc(hidden)]
848        #[allow(non_snake_case)]
849        fn #schema_reg_fn_name(spec: &mut #rustapi_path::__private::openapi::OpenApiSpec) {
850            #( spec.register_in_place::<#schema_types>(); )*
851        }
852
853        // Auto-register schema population function (linkme).
854        // See the route registration above for why the `#[linkme(crate = ...)]`
855        // attribute is present.
856        #[doc(hidden)]
857        #[allow(non_upper_case_globals)]
858        // Schema registration via linkme (for the `#[schema]` attribute on types)
859        #[#rustapi_path::__private::linkme::distributed_slice(#rustapi_path::__private::AUTO_SCHEMAS)]
860        #[linkme(crate = #rustapi_path::__private::linkme)]
861        static #auto_schema_name: fn(&mut #rustapi_path::__private::openapi::OpenApiSpec) = #schema_reg_fn_name;
862    };
863
864    debug_output(&format!("{} {}", method, path_value), &expanded);
865
866    TokenStream::from(expanded)
867}
868
869/// GET route handler macro
870///
871/// # Example
872///
873/// ```rust,ignore
874/// #[rustapi::get("/users")]
875/// async fn list_users() -> Json<Vec<User>> {
876///     Json(vec![])
877/// }
878///
879/// #[rustapi::get("/users/{id}")]
880/// async fn get_user(Path(id): Path<i64>) -> Result<User> {
881///     Ok(User { id, name: "John".into() })
882/// }
883/// ```
884#[proc_macro_attribute]
885pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
886    generate_route_handler("GET", attr, item)
887}
888
889/// POST route handler macro
890#[proc_macro_attribute]
891pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
892    generate_route_handler("POST", attr, item)
893}
894
895/// PUT route handler macro
896#[proc_macro_attribute]
897pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
898    generate_route_handler("PUT", attr, item)
899}
900
901/// PATCH route handler macro
902#[proc_macro_attribute]
903pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
904    generate_route_handler("PATCH", attr, item)
905}
906
907/// DELETE route handler macro
908#[proc_macro_attribute]
909pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
910    generate_route_handler("DELETE", attr, item)
911}
912
913// ============================================
914// Route Metadata Macros
915// ============================================
916
917/// Tag macro for grouping endpoints in OpenAPI documentation
918///
919/// # Example
920///
921/// ```rust,ignore
922/// #[rustapi::get("/users")]
923/// #[rustapi::tag("Users")]
924/// async fn list_users() -> Json<Vec<User>> {
925///     Json(vec![])
926/// }
927/// ```
928#[proc_macro_attribute]
929pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
930    let tag = parse_macro_input!(attr as LitStr);
931    let input = parse_macro_input!(item as ItemFn);
932
933    let attrs = &input.attrs;
934    let vis = &input.vis;
935    let sig = &input.sig;
936    let block = &input.block;
937    let tag_value = tag.value();
938
939    // Add a doc comment with the tag info for documentation
940    let expanded = quote! {
941        #[doc = concat!("**Tag:** ", #tag_value)]
942        #(#attrs)*
943        #vis #sig #block
944    };
945
946    TokenStream::from(expanded)
947}
948
949/// Summary macro for endpoint summary in OpenAPI documentation
950///
951/// # Example
952///
953/// ```rust,ignore
954/// #[rustapi::get("/users")]
955/// #[rustapi::summary("List all users")]
956/// async fn list_users() -> Json<Vec<User>> {
957///     Json(vec![])
958/// }
959/// ```
960#[proc_macro_attribute]
961pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
962    let summary = parse_macro_input!(attr as LitStr);
963    let input = parse_macro_input!(item as ItemFn);
964
965    let attrs = &input.attrs;
966    let vis = &input.vis;
967    let sig = &input.sig;
968    let block = &input.block;
969    let summary_value = summary.value();
970
971    // Add a doc comment with the summary
972    let expanded = quote! {
973        #[doc = #summary_value]
974        #(#attrs)*
975        #vis #sig #block
976    };
977
978    TokenStream::from(expanded)
979}
980
981/// Description macro for detailed endpoint description in OpenAPI documentation
982///
983/// # Example
984///
985/// ```rust,ignore
986/// #[rustapi::get("/users")]
987/// #[rustapi::description("Returns a list of all users in the system. Supports pagination.")]
988/// async fn list_users() -> Json<Vec<User>> {
989///     Json(vec![])
990/// }
991/// ```
992#[proc_macro_attribute]
993pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
994    let desc = parse_macro_input!(attr as LitStr);
995    let input = parse_macro_input!(item as ItemFn);
996
997    let attrs = &input.attrs;
998    let vis = &input.vis;
999    let sig = &input.sig;
1000    let block = &input.block;
1001    let desc_value = desc.value();
1002
1003    // Add a doc comment with the description
1004    let expanded = quote! {
1005        #[doc = ""]
1006        #[doc = #desc_value]
1007        #(#attrs)*
1008        #vis #sig #block
1009    };
1010
1011    TokenStream::from(expanded)
1012}
1013
1014/// MCP metadata attribute for controlling how an endpoint is exposed as an MCP tool.
1015///
1016/// Supports:
1017/// - `#[mcp(skip)]` — never expose this route as a tool
1018/// - `#[mcp(readonly)]` — treat as read-only even if it's a POST etc.
1019/// - `#[mcp(write)]` — mark as a write operation
1020/// - `#[mcp(require = "confirm")]` — agent should ask for confirmation
1021///
1022/// # Example
1023///
1024/// ```rust,ignore
1025/// #[rustapi::get("/admin/secrets")]
1026/// #[rustapi::mcp(skip)]
1027/// async fn admin_secrets() -> &'static str { "secret" }
1028///
1029/// #[rustapi::post("/orders")]
1030/// #[rustapi::mcp(write, require = "confirm")]
1031/// async fn create_order(...) { ... }
1032/// ```
1033#[proc_macro_attribute]
1034pub fn mcp(_attr: TokenStream, item: TokenStream) -> TokenStream {
1035    // This is a passthrough. The actual semantics are handled by the
1036    // route macros (get/post/...) which inspect #[mcp(...)] attrs on the fn.
1037    item
1038}
1039
1040/// Path parameter schema macro for OpenAPI documentation
1041///
1042/// Use this to specify the OpenAPI schema type for a path parameter when
1043/// the auto-inferred type is incorrect. This is particularly useful for
1044/// UUID parameters that might be named `id`.
1045///
1046/// # Supported schema types
1047/// - `"uuid"` - String with UUID format
1048/// - `"integer"` or `"int"` - Integer with int64 format
1049/// - `"string"` - Plain string
1050/// - `"boolean"` or `"bool"` - Boolean
1051/// - `"number"` - Number (float)
1052///
1053/// # Example
1054///
1055/// ```rust,ignore
1056/// use uuid::Uuid;
1057///
1058/// #[rustapi::get("/users/{id}")]
1059/// #[rustapi::param(id, schema = "uuid")]
1060/// async fn get_user(Path(id): Path<Uuid>) -> Json<User> {
1061///     // ...
1062/// }
1063///
1064/// // Alternative syntax:
1065/// #[rustapi::get("/posts/{post_id}")]
1066/// #[rustapi::param(post_id = "uuid")]
1067/// async fn get_post(Path(post_id): Path<Uuid>) -> Json<Post> {
1068///     // ...
1069/// }
1070/// ```
1071#[proc_macro_attribute]
1072pub fn param(_attr: TokenStream, item: TokenStream) -> TokenStream {
1073    // The param attribute is processed by the route macro (get, post, etc.)
1074    // This macro just passes through the function unchanged
1075    item
1076}
1077
1078/// Error responses macro for OpenAPI documentation
1079///
1080/// Declares possible error responses for a handler endpoint. These are
1081/// automatically added to the OpenAPI specification.
1082///
1083/// # Syntax
1084///
1085/// ```rust,ignore
1086/// #[rustapi::errors(404 = "User not found", 403 = "Access denied", 409 = "Email already exists")]
1087/// ```
1088///
1089/// # Example
1090///
1091/// ```rust,ignore
1092/// #[rustapi::get("/users/{id}")]
1093/// #[rustapi::errors(404 = "User not found", 403 = "Forbidden")]
1094/// async fn get_user(Path(id): Path<Uuid>) -> Result<Json<User>> {
1095///     // ...
1096/// }
1097/// ```
1098///
1099/// This generates OpenAPI responses for 404 and 403 status codes,
1100/// each referencing the standard ErrorSchema component.
1101#[proc_macro_attribute]
1102pub fn errors(_attr: TokenStream, item: TokenStream) -> TokenStream {
1103    // The errors attribute is processed by the route macro (get, post, etc.)
1104    // This macro just passes through the function unchanged
1105    item
1106}
1107
1108// ============================================
1109// Validation Derive Macro
1110// ============================================
1111
1112/// Parsed validation rule from field attributes
1113#[derive(Debug)]
1114struct ValidationRuleInfo {
1115    rule_type: String,
1116    params: Vec<(String, String)>,
1117    message: Option<String>,
1118    groups: Vec<String>,
1119}
1120
1121/// Parse validation attributes from a field
1122fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
1123    let mut rules = Vec::new();
1124
1125    for attr in attrs {
1126        if !attr.path().is_ident("validate") {
1127            continue;
1128        }
1129
1130        // Parse the validate attribute
1131        if let Ok(meta) = attr.parse_args::<Meta>() {
1132            if let Some(rule) = parse_validate_meta(&meta) {
1133                rules.push(rule);
1134            }
1135        } else if let Ok(nested) = attr
1136            .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
1137        {
1138            for meta in nested {
1139                if let Some(rule) = parse_validate_meta(&meta) {
1140                    rules.push(rule);
1141                }
1142            }
1143        }
1144    }
1145
1146    rules
1147}
1148
1149/// Parse a single validation meta item
1150fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
1151    match meta {
1152        Meta::Path(path) => {
1153            // Simple rule like #[validate(email)]
1154            let ident = path.get_ident()?.to_string();
1155            Some(ValidationRuleInfo {
1156                rule_type: ident,
1157                params: Vec::new(),
1158                message: None,
1159                groups: Vec::new(),
1160            })
1161        }
1162        Meta::List(list) => {
1163            // Rule with params like #[validate(length(min = 3, max = 50))]
1164            let rule_type = list.path.get_ident()?.to_string();
1165            let mut params = Vec::new();
1166            let mut message = None;
1167            let mut groups = Vec::new();
1168
1169            // Parse nested params
1170            if let Ok(nested) = list.parse_args_with(
1171                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
1172            ) {
1173                for nested_meta in nested {
1174                    if let Meta::NameValue(nv) = &nested_meta {
1175                        let key = nv.path.get_ident()?.to_string();
1176
1177                        if key == "groups" {
1178                            let vec = expr_to_string_vec(&nv.value);
1179                            groups.extend(vec);
1180                        } else if let Some(value) = expr_to_string(&nv.value) {
1181                            if key == "message" {
1182                                message = Some(value);
1183                            } else if key == "group" {
1184                                groups.push(value);
1185                            } else {
1186                                params.push((key, value));
1187                            }
1188                        }
1189                    } else if let Meta::Path(path) = &nested_meta {
1190                        // Handle flags like #[validate(ip(v4))]
1191                        if let Some(ident) = path.get_ident() {
1192                            params.push((ident.to_string(), "true".to_string()));
1193                        }
1194                    }
1195                }
1196            }
1197
1198            Some(ValidationRuleInfo {
1199                rule_type,
1200                params,
1201                message,
1202                groups,
1203            })
1204        }
1205        Meta::NameValue(nv) => {
1206            // Rule like #[validate(regex = "pattern")]
1207            let rule_type = nv.path.get_ident()?.to_string();
1208            let value = expr_to_string(&nv.value)?;
1209
1210            Some(ValidationRuleInfo {
1211                rule_type: rule_type.clone(),
1212                params: vec![(rule_type.clone(), value)],
1213                message: None,
1214                groups: Vec::new(),
1215            })
1216        }
1217    }
1218}
1219
1220/// Convert an expression to a string value
1221fn expr_to_string(expr: &Expr) -> Option<String> {
1222    match expr {
1223        Expr::Lit(lit) => match &lit.lit {
1224            Lit::Str(s) => Some(s.value()),
1225            Lit::Int(i) => Some(i.base10_digits().to_string()),
1226            Lit::Float(f) => Some(f.base10_digits().to_string()),
1227            Lit::Bool(b) => Some(b.value.to_string()),
1228            _ => None,
1229        },
1230        _ => None,
1231    }
1232}
1233
1234/// Convert an expression to a vector of strings
1235fn expr_to_string_vec(expr: &Expr) -> Vec<String> {
1236    match expr {
1237        Expr::Array(arr) => {
1238            let mut result = Vec::new();
1239            for elem in &arr.elems {
1240                if let Some(s) = expr_to_string(elem) {
1241                    result.push(s);
1242                }
1243            }
1244            result
1245        }
1246        _ => {
1247            if let Some(s) = expr_to_string(expr) {
1248                vec![s]
1249            } else {
1250                Vec::new()
1251            }
1252        }
1253    }
1254}
1255
1256/// Determine the path to rustapi_validate based on the user's dependencies.
1257///
1258/// Checks for (in order):
1259/// 1. `rustapi-rs` → `::rustapi_rs::__private::rustapi_validate`
1260/// 2. `rustapi-validate` → `::rustapi_validate`
1261///
1262/// This allows the Validate derive macro to work in both user projects
1263/// (which depend on rustapi-rs) and internal crates (which depend on
1264/// rustapi-validate directly).
1265fn get_validate_path() -> proc_macro2::TokenStream {
1266    let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1267
1268    if let Ok(found) = rustapi_rs_found {
1269        match found {
1270            FoundCrate::Itself => {
1271                quote! { ::rustapi_rs::__private::validate }
1272            }
1273            FoundCrate::Name(name) => {
1274                let normalized = name.replace('-', "_");
1275                let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1276                quote! { ::#ident::__private::validate }
1277            }
1278        }
1279    } else if let Ok(found) =
1280        crate_name("rustapi-validate").or_else(|_| crate_name("rustapi_validate"))
1281    {
1282        match found {
1283            FoundCrate::Itself => quote! { crate },
1284            FoundCrate::Name(name) => {
1285                let normalized = name.replace('-', "_");
1286                let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1287                quote! { ::#ident }
1288            }
1289        }
1290    } else {
1291        // Default fallback
1292        quote! { ::rustapi_validate }
1293    }
1294}
1295
1296/// Determine the path to rustapi_core based on the user's dependencies.
1297///
1298/// Checks for (in order):
1299/// 1. `rustapi-rs` (which re-exports rustapi-core via glob)
1300/// 2. `rustapi-core` directly
1301fn get_core_path() -> proc_macro2::TokenStream {
1302    let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1303
1304    if let Ok(found) = rustapi_rs_found {
1305        match found {
1306            FoundCrate::Itself => quote! { ::rustapi_rs::__private::core },
1307            FoundCrate::Name(name) => {
1308                let normalized = name.replace('-', "_");
1309                let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1310                quote! { ::#ident::__private::core }
1311            }
1312        }
1313    } else if let Ok(found) = crate_name("rustapi-core").or_else(|_| crate_name("rustapi_core")) {
1314        match found {
1315            FoundCrate::Itself => quote! { crate },
1316            FoundCrate::Name(name) => {
1317                let normalized = name.replace('-', "_");
1318                let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1319                quote! { ::#ident }
1320            }
1321        }
1322    } else {
1323        quote! { ::rustapi_core }
1324    }
1325}
1326
1327/// Determine the path to async_trait based on the user's dependencies.
1328///
1329/// Checks for (in order):
1330/// 1. `rustapi-rs` → `::rustapi_rs::__private::async_trait`
1331/// 2. `async-trait` directly
1332fn get_async_trait_path() -> proc_macro2::TokenStream {
1333    let rustapi_rs_found = crate_name("rustapi-rs").or_else(|_| crate_name("rustapi_rs"));
1334
1335    if let Ok(found) = rustapi_rs_found {
1336        match found {
1337            FoundCrate::Itself => {
1338                quote! { ::rustapi_rs::__private::async_trait }
1339            }
1340            FoundCrate::Name(name) => {
1341                let normalized = name.replace('-', "_");
1342                let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1343                quote! { ::#ident::__private::async_trait }
1344            }
1345        }
1346    } else if let Ok(found) = crate_name("async-trait").or_else(|_| crate_name("async_trait")) {
1347        match found {
1348            FoundCrate::Itself => quote! { crate },
1349            FoundCrate::Name(name) => {
1350                let normalized = name.replace('-', "_");
1351                let ident = syn::Ident::new(&normalized, proc_macro2::Span::call_site());
1352                quote! { ::#ident }
1353            }
1354        }
1355    } else {
1356        quote! { ::async_trait }
1357    }
1358}
1359
1360fn generate_rule_validation(
1361    field_name: &str,
1362    _field_type: &Type,
1363    rule: &ValidationRuleInfo,
1364    validate_path: &proc_macro2::TokenStream,
1365) -> proc_macro2::TokenStream {
1366    let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1367    let field_name_str = field_name;
1368
1369    // Generate group check
1370    let group_check = if rule.groups.is_empty() {
1371        quote! { true }
1372    } else {
1373        let group_names = rule.groups.iter().map(|g| g.as_str());
1374        quote! {
1375            {
1376                let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1377                rule_groups.iter().any(|g| g.matches(&group))
1378            }
1379        }
1380    };
1381
1382    let validation_logic = match rule.rule_type.as_str() {
1383        "email" => {
1384            let message = rule
1385                .message
1386                .as_ref()
1387                .map(|m| quote! { .with_message(#m) })
1388                .unwrap_or_default();
1389            quote! {
1390                {
1391                    let rule = #validate_path::v2::EmailRule::new() #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        "length" => {
1399            let min = rule
1400                .params
1401                .iter()
1402                .find(|(k, _)| k == "min")
1403                .and_then(|(_, v)| v.parse::<usize>().ok());
1404            let max = rule
1405                .params
1406                .iter()
1407                .find(|(k, _)| k == "max")
1408                .and_then(|(_, v)| v.parse::<usize>().ok());
1409            let message = rule
1410                .message
1411                .as_ref()
1412                .map(|m| quote! { .with_message(#m) })
1413                .unwrap_or_default();
1414
1415            let rule_creation = match (min, max) {
1416                (Some(min), Some(max)) => {
1417                    quote! { #validate_path::v2::LengthRule::new(#min, #max) }
1418                }
1419                (Some(min), None) => quote! { #validate_path::v2::LengthRule::min(#min) },
1420                (None, Some(max)) => quote! { #validate_path::v2::LengthRule::max(#max) },
1421                (None, None) => quote! { #validate_path::v2::LengthRule::new(0, usize::MAX) },
1422            };
1423
1424            quote! {
1425                {
1426                    let rule = #rule_creation #message;
1427                    if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1428                        errors.add(#field_name_str, e);
1429                    }
1430                }
1431            }
1432        }
1433        "range" => {
1434            let min = rule
1435                .params
1436                .iter()
1437                .find(|(k, _)| k == "min")
1438                .map(|(_, v)| v.clone());
1439            let max = rule
1440                .params
1441                .iter()
1442                .find(|(k, _)| k == "max")
1443                .map(|(_, v)| v.clone());
1444            let message = rule
1445                .message
1446                .as_ref()
1447                .map(|m| quote! { .with_message(#m) })
1448                .unwrap_or_default();
1449
1450            // Determine the numeric type from the field type
1451            let rule_creation = match (min, max) {
1452                (Some(min), Some(max)) => {
1453                    let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1454                    let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1455                    quote! { #validate_path::v2::RangeRule::new(#min_lit, #max_lit) }
1456                }
1457                (Some(min), None) => {
1458                    let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
1459                    quote! { #validate_path::v2::RangeRule::min(#min_lit) }
1460                }
1461                (None, Some(max)) => {
1462                    let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
1463                    quote! { #validate_path::v2::RangeRule::max(#max_lit) }
1464                }
1465                (None, None) => {
1466                    return quote! {};
1467                }
1468            };
1469
1470            quote! {
1471                {
1472                    let rule = #rule_creation #message;
1473                    if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1474                        errors.add(#field_name_str, e);
1475                    }
1476                }
1477            }
1478        }
1479        "regex" => {
1480            let pattern = rule
1481                .params
1482                .iter()
1483                .find(|(k, _)| k == "regex" || k == "pattern")
1484                .map(|(_, v)| v.clone())
1485                .unwrap_or_default();
1486            let message = rule
1487                .message
1488                .as_ref()
1489                .map(|m| quote! { .with_message(#m) })
1490                .unwrap_or_default();
1491
1492            quote! {
1493                {
1494                    let rule = #validate_path::v2::RegexRule::new(#pattern) #message;
1495                    if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1496                        errors.add(#field_name_str, e);
1497                    }
1498                }
1499            }
1500        }
1501        "url" => {
1502            let message = rule
1503                .message
1504                .as_ref()
1505                .map(|m| quote! { .with_message(#m) })
1506                .unwrap_or_default();
1507            quote! {
1508                {
1509                    let rule = #validate_path::v2::UrlRule::new() #message;
1510                    if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1511                        errors.add(#field_name_str, e);
1512                    }
1513                }
1514            }
1515        }
1516        "required" => {
1517            let message = rule
1518                .message
1519                .as_ref()
1520                .map(|m| quote! { .with_message(#m) })
1521                .unwrap_or_default();
1522            quote! {
1523                {
1524                    let rule = #validate_path::v2::RequiredRule::new() #message;
1525                    if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1526                        errors.add(#field_name_str, e);
1527                    }
1528                }
1529            }
1530        }
1531        "credit_card" => {
1532            let message = rule
1533                .message
1534                .as_ref()
1535                .map(|m| quote! { .with_message(#m) })
1536                .unwrap_or_default();
1537            quote! {
1538                {
1539                    let rule = #validate_path::v2::CreditCardRule::new() #message;
1540                    if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1541                        errors.add(#field_name_str, e);
1542                    }
1543                }
1544            }
1545        }
1546        "ip" => {
1547            let v4 = rule.params.iter().any(|(k, _)| k == "v4");
1548            let v6 = rule.params.iter().any(|(k, _)| k == "v6");
1549
1550            let rule_creation = if v4 && !v6 {
1551                quote! { #validate_path::v2::IpRule::v4() }
1552            } else if !v4 && v6 {
1553                quote! { #validate_path::v2::IpRule::v6() }
1554            } else {
1555                quote! { #validate_path::v2::IpRule::new() }
1556            };
1557
1558            let message = rule
1559                .message
1560                .as_ref()
1561                .map(|m| quote! { .with_message(#m) })
1562                .unwrap_or_default();
1563
1564            quote! {
1565                {
1566                    let rule = #rule_creation #message;
1567                    if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1568                        errors.add(#field_name_str, e);
1569                    }
1570                }
1571            }
1572        }
1573        "phone" => {
1574            let message = rule
1575                .message
1576                .as_ref()
1577                .map(|m| quote! { .with_message(#m) })
1578                .unwrap_or_default();
1579            quote! {
1580                {
1581                    let rule = #validate_path::v2::PhoneRule::new() #message;
1582                    if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1583                        errors.add(#field_name_str, e);
1584                    }
1585                }
1586            }
1587        }
1588        "contains" => {
1589            let needle = rule
1590                .params
1591                .iter()
1592                .find(|(k, _)| k == "needle")
1593                .map(|(_, v)| v.clone())
1594                .unwrap_or_default();
1595
1596            let message = rule
1597                .message
1598                .as_ref()
1599                .map(|m| quote! { .with_message(#m) })
1600                .unwrap_or_default();
1601
1602            quote! {
1603                {
1604                    let rule = #validate_path::v2::ContainsRule::new(#needle) #message;
1605                    if let Err(e) = #validate_path::v2::ValidationRule::validate(&rule, &self.#field_ident) {
1606                        errors.add(#field_name_str, e);
1607                    }
1608                }
1609            }
1610        }
1611        _ => {
1612            // Unknown rule - skip
1613            quote! {}
1614        }
1615    };
1616
1617    quote! {
1618        if #group_check {
1619            #validation_logic
1620        }
1621    }
1622}
1623
1624/// Generate async validation code for a single rule
1625fn generate_async_rule_validation(
1626    field_name: &str,
1627    rule: &ValidationRuleInfo,
1628    validate_path: &proc_macro2::TokenStream,
1629) -> proc_macro2::TokenStream {
1630    let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
1631    let field_name_str = field_name;
1632
1633    // Generate group check
1634    let group_check = if rule.groups.is_empty() {
1635        quote! { true }
1636    } else {
1637        let group_names = rule.groups.iter().map(|g| g.as_str());
1638        quote! {
1639            {
1640                let rule_groups = [#(#validate_path::v2::ValidationGroup::from(#group_names)),*];
1641                rule_groups.iter().any(|g| g.matches(&group))
1642            }
1643        }
1644    };
1645
1646    let validation_logic = match rule.rule_type.as_str() {
1647        "async_unique" => {
1648            let table = rule
1649                .params
1650                .iter()
1651                .find(|(k, _)| k == "table")
1652                .map(|(_, v)| v.clone())
1653                .unwrap_or_default();
1654            let column = rule
1655                .params
1656                .iter()
1657                .find(|(k, _)| k == "column")
1658                .map(|(_, v)| v.clone())
1659                .unwrap_or_default();
1660            let message = rule
1661                .message
1662                .as_ref()
1663                .map(|m| quote! { .with_message(#m) })
1664                .unwrap_or_default();
1665
1666            quote! {
1667                {
1668                    let rule = #validate_path::v2::AsyncUniqueRule::new(#table, #column) #message;
1669                    if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1670                        errors.add(#field_name_str, e);
1671                    }
1672                }
1673            }
1674        }
1675        "async_exists" => {
1676            let table = rule
1677                .params
1678                .iter()
1679                .find(|(k, _)| k == "table")
1680                .map(|(_, v)| v.clone())
1681                .unwrap_or_default();
1682            let column = rule
1683                .params
1684                .iter()
1685                .find(|(k, _)| k == "column")
1686                .map(|(_, v)| v.clone())
1687                .unwrap_or_default();
1688            let message = rule
1689                .message
1690                .as_ref()
1691                .map(|m| quote! { .with_message(#m) })
1692                .unwrap_or_default();
1693
1694            quote! {
1695                {
1696                    let rule = #validate_path::v2::AsyncExistsRule::new(#table, #column) #message;
1697                    if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1698                        errors.add(#field_name_str, e);
1699                    }
1700                }
1701            }
1702        }
1703        "async_api" => {
1704            let endpoint = rule
1705                .params
1706                .iter()
1707                .find(|(k, _)| k == "endpoint")
1708                .map(|(_, v)| v.clone())
1709                .unwrap_or_default();
1710            let message = rule
1711                .message
1712                .as_ref()
1713                .map(|m| quote! { .with_message(#m) })
1714                .unwrap_or_default();
1715
1716            quote! {
1717                {
1718                    let rule = #validate_path::v2::AsyncApiRule::new(#endpoint) #message;
1719                    if let Err(e) = #validate_path::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
1720                        errors.add(#field_name_str, e);
1721                    }
1722                }
1723            }
1724        }
1725        "custom_async" => {
1726            // #[validate(custom_async = "function_path")]
1727            let function_path = rule
1728                .params
1729                .iter()
1730                .find(|(k, _)| k == "custom_async" || k == "function")
1731                .map(|(_, v)| v.clone())
1732                .unwrap_or_default();
1733
1734            if function_path.is_empty() {
1735                // If path is missing, don't generate invalid code
1736                quote! {}
1737            } else {
1738                let func: syn::Path = syn::parse_str(&function_path).unwrap();
1739                let message_handling = if let Some(msg) = &rule.message {
1740                    quote! {
1741                        let e = #validate_path::v2::RuleError::new("custom_async", #msg);
1742                        errors.add(#field_name_str, e);
1743                    }
1744                } else {
1745                    quote! {
1746                        errors.add(#field_name_str, e);
1747                    }
1748                };
1749
1750                quote! {
1751                    {
1752                        // Call the custom async function: async fn(&T, &ValidationContext) -> Result<(), RuleError>
1753                        if let Err(e) = #func(&self.#field_ident, ctx).await {
1754                            #message_handling
1755                        }
1756                    }
1757                }
1758            }
1759        }
1760        _ => {
1761            // Not an async rule
1762            quote! {}
1763        }
1764    };
1765
1766    quote! {
1767        if #group_check {
1768            #validation_logic
1769        }
1770    }
1771}
1772
1773/// Check if a rule is async
1774fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
1775    matches!(
1776        rule.rule_type.as_str(),
1777        "async_unique" | "async_exists" | "async_api" | "custom_async"
1778    )
1779}
1780
1781/// Derive macro for implementing Validate and AsyncValidate traits
1782///
1783/// # Example
1784///
1785/// ```rust,ignore
1786/// use rustapi_macros::Validate;
1787///
1788/// #[derive(Validate)]
1789/// struct CreateUser {
1790///     #[validate(email, message = "Invalid email format")]
1791///     email: String,
1792///     
1793///     #[validate(length(min = 3, max = 50))]
1794///     username: String,
1795///     
1796///     #[validate(range(min = 18, max = 120))]
1797///     age: u8,
1798///     
1799///     #[validate(async_unique(table = "users", column = "email"))]
1800///     email: String,
1801/// }
1802/// ```
1803#[proc_macro_derive(Validate, attributes(validate))]
1804pub fn derive_validate(input: TokenStream) -> TokenStream {
1805    let input = parse_macro_input!(input as DeriveInput);
1806    let name = &input.ident;
1807    let generics = &input.generics;
1808    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1809
1810    // Only support structs with named fields
1811    let fields = match &input.data {
1812        Data::Struct(data) => match &data.fields {
1813            Fields::Named(fields) => &fields.named,
1814            _ => {
1815                return syn::Error::new_spanned(
1816                    &input,
1817                    "Validate can only be derived for structs with named fields",
1818                )
1819                .to_compile_error()
1820                .into();
1821            }
1822        },
1823        _ => {
1824            return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1825                .to_compile_error()
1826                .into();
1827        }
1828    };
1829
1830    // Resolve crate paths dynamically based on the caller's dependencies
1831    let validate_path = get_validate_path();
1832    let core_path = get_core_path();
1833    let async_trait_path = get_async_trait_path();
1834
1835    // Collect sync and async validation code for each field
1836    let mut sync_validations = Vec::new();
1837    let mut async_validations = Vec::new();
1838    let mut has_async_rules = false;
1839
1840    for field in fields {
1841        let field_name = field.ident.as_ref().unwrap().to_string();
1842        let field_type = &field.ty;
1843        let rules = parse_validate_attrs(&field.attrs);
1844
1845        for rule in &rules {
1846            if is_async_rule(rule) {
1847                has_async_rules = true;
1848                let validation = generate_async_rule_validation(&field_name, rule, &validate_path);
1849                async_validations.push(validation);
1850            } else {
1851                let validation =
1852                    generate_rule_validation(&field_name, field_type, rule, &validate_path);
1853                sync_validations.push(validation);
1854            }
1855        }
1856    }
1857
1858    // Generate the Validate impl
1859    let validate_impl = quote! {
1860        impl #impl_generics #validate_path::v2::Validate for #name #ty_generics #where_clause {
1861            fn validate_with_group(&self, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1862                let mut errors = #validate_path::v2::ValidationErrors::new();
1863
1864                #(#sync_validations)*
1865
1866                errors.into_result()
1867            }
1868        }
1869    };
1870
1871    // Generate the AsyncValidate impl if there are async rules
1872    let async_validate_impl = if has_async_rules {
1873        quote! {
1874            #[#async_trait_path::async_trait]
1875            impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1876                async fn validate_async_with_group(&self, ctx: &#validate_path::v2::ValidationContext, group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1877                    let mut errors = #validate_path::v2::ValidationErrors::new();
1878
1879                    #(#async_validations)*
1880
1881                    errors.into_result()
1882                }
1883            }
1884        }
1885    } else {
1886        // Provide a default AsyncValidate impl that just returns Ok
1887        quote! {
1888            #[#async_trait_path::async_trait]
1889            impl #impl_generics #validate_path::v2::AsyncValidate for #name #ty_generics #where_clause {
1890                async fn validate_async_with_group(&self, _ctx: &#validate_path::v2::ValidationContext, _group: #validate_path::v2::ValidationGroup) -> Result<(), #validate_path::v2::ValidationErrors> {
1891                    Ok(())
1892                }
1893            }
1894        }
1895    };
1896
1897    // Generate the Validatable impl for rustapi-core integration (exposed via rustapi-rs)
1898    // Paths are resolved dynamically so this works from both rustapi-rs and internal crates.
1899    let validatable_impl = quote! {
1900        impl #impl_generics #core_path::validation::Validatable for #name #ty_generics #where_clause {
1901            fn do_validate(&self) -> Result<(), #core_path::ApiError> {
1902                match #validate_path::v2::Validate::validate(self) {
1903                    Ok(_) => Ok(()),
1904                    Err(e) => Err(#core_path::validation::convert_v2_errors(e)),
1905                }
1906            }
1907        }
1908    };
1909
1910    let expanded = quote! {
1911        #validate_impl
1912        #async_validate_impl
1913        #validatable_impl
1914    };
1915
1916    debug_output("Validate derive", &expanded);
1917
1918    TokenStream::from(expanded)
1919}
1920
1921// ============================================
1922// ApiError Derive Macro
1923// ============================================
1924
1925/// Derive macro for implementing IntoResponse for error enums
1926///
1927/// # Example
1928///
1929/// ```rust,ignore
1930/// #[derive(ApiError)]
1931/// enum UserError {
1932///     #[error(status = 404, message = "User not found")]
1933///     NotFound(i64),
1934///     
1935///     #[error(status = 400, code = "validation_error")]
1936///     InvalidInput(String),
1937/// }
1938/// ```
1939#[proc_macro_derive(ApiError, attributes(error))]
1940pub fn derive_api_error(input: TokenStream) -> TokenStream {
1941    api_error::expand_derive_api_error(input)
1942}
1943
1944// ============================================
1945// TypedPath Derive Macro
1946// ============================================
1947
1948/// Derive macro for TypedPath
1949///
1950/// # Example
1951///
1952/// ```rust,ignore
1953/// #[derive(TypedPath, Deserialize, Serialize)]
1954/// #[typed_path("/users/{id}/posts/{post_id}")]
1955/// struct PostPath {
1956///     id: u64,
1957///     post_id: String,
1958/// }
1959/// ```
1960#[proc_macro_derive(TypedPath, attributes(typed_path))]
1961pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1962    let input = parse_macro_input!(input as DeriveInput);
1963    let name = &input.ident;
1964    let generics = &input.generics;
1965    let rustapi_path = get_rustapi_path();
1966    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1967
1968    // Find the #[typed_path("...")] attribute
1969    let mut path_str = None;
1970    for attr in &input.attrs {
1971        if attr.path().is_ident("typed_path") {
1972            if let Ok(lit) = attr.parse_args::<LitStr>() {
1973                path_str = Some(lit.value());
1974            }
1975        }
1976    }
1977
1978    let path = match path_str {
1979        Some(p) => p,
1980        None => {
1981            return syn::Error::new_spanned(
1982                &input,
1983                "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1984            )
1985            .to_compile_error()
1986            .into();
1987        }
1988    };
1989
1990    // Validate path syntax
1991    if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1992        return err.to_compile_error().into();
1993    }
1994
1995    // Generate to_uri implementation
1996    // We need to parse the path and replace {param} with self.param
1997    let mut format_string = String::new();
1998    let mut format_args = Vec::new();
1999
2000    let mut chars = path.chars().peekable();
2001    while let Some(ch) = chars.next() {
2002        if ch == '{' {
2003            let mut param_name = String::new();
2004            while let Some(&c) = chars.peek() {
2005                if c == '}' {
2006                    chars.next(); // Consume '}'
2007                    break;
2008                }
2009                param_name.push(chars.next().unwrap());
2010            }
2011
2012            if param_name.is_empty() {
2013                return syn::Error::new_spanned(
2014                    &input,
2015                    "Empty path parameter not allowed in typed_path",
2016                )
2017                .to_compile_error()
2018                .into();
2019            }
2020
2021            format_string.push_str("{}");
2022            let ident = syn::Ident::new(&param_name, proc_macro2::Span::call_site());
2023            format_args.push(quote! { self.#ident });
2024        } else {
2025            format_string.push(ch);
2026        }
2027    }
2028
2029    let expanded = quote! {
2030        impl #impl_generics #rustapi_path::prelude::TypedPath for #name #ty_generics #where_clause {
2031            const PATH: &'static str = #path;
2032
2033            fn to_uri(&self) -> String {
2034                format!(#format_string, #(#format_args),*)
2035            }
2036        }
2037    };
2038
2039    debug_output("TypedPath derive", &expanded);
2040    TokenStream::from(expanded)
2041}