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