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