Skip to main content

rustapi_macros/
lib.rs

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