rustapi_macros/
lib.rs

1//! Procedural macros for RustAPI
2//!
3//! This crate provides the attribute macros used in RustAPI:
4//!
5//! - `#[rustapi::main]` - Main entry point macro
6//! - `#[rustapi::get("/path")]` - GET route handler
7//! - `#[rustapi::post("/path")]` - POST route handler
8//! - `#[rustapi::put("/path")]` - PUT route handler
9//! - `#[rustapi::patch("/path")]` - PATCH route handler
10//! - `#[rustapi::delete("/path")]` - DELETE route handler
11//! - `#[derive(Validate)]` - Validation derive macro
12//!
13//! ## Debugging
14//!
15//! Set `RUSTAPI_DEBUG=1` environment variable during compilation to see
16//! expanded macro output for debugging purposes.
17
18use proc_macro::TokenStream;
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
26/// Auto-register a schema type for zero-config OpenAPI.
27///
28/// Attach this to a `struct` or `enum` that also derives `Schema` (utoipa::ToSchema).
29/// This ensures the type is registered into RustAPI's OpenAPI components even if it is
30/// only referenced indirectly (e.g. as a nested field type).
31///
32/// ```rust,ignore
33/// use rustapi_rs::prelude::*;
34///
35/// #[rustapi_rs::schema]
36/// #[derive(Serialize, Schema)]
37/// struct UserInfo { /* ... */ }
38/// ```
39#[proc_macro_attribute]
40pub fn schema(_attr: TokenStream, item: TokenStream) -> TokenStream {
41    let input = parse_macro_input!(item as syn::Item);
42
43    let (ident, generics) = match &input {
44        syn::Item::Struct(s) => (&s.ident, &s.generics),
45        syn::Item::Enum(e) => (&e.ident, &e.generics),
46        _ => {
47            return syn::Error::new_spanned(
48                &input,
49                "#[rustapi_rs::schema] can only be used on structs or enums",
50            )
51            .to_compile_error()
52            .into();
53        }
54    };
55
56    if !generics.params.is_empty() {
57        return syn::Error::new_spanned(
58            generics,
59            "#[rustapi_rs::schema] does not support generic types",
60        )
61        .to_compile_error()
62        .into();
63    }
64
65    let registrar_ident = syn::Ident::new(
66        &format!("__RUSTAPI_AUTO_SCHEMA_{}", ident),
67        proc_macro2::Span::call_site(),
68    );
69
70    let expanded = quote! {
71        #input
72
73        #[allow(non_upper_case_globals)]
74        #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
75        #[linkme(crate = ::rustapi_rs::__private::linkme)]
76        static #registrar_ident: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) =
77            |spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec| {
78                spec.register_in_place::<#ident>();
79            };
80    };
81
82    debug_output("schema", &expanded);
83    expanded.into()
84}
85
86fn extract_schema_types(ty: &Type, out: &mut Vec<Type>, allow_leaf: bool) {
87    match ty {
88        Type::Reference(r) => extract_schema_types(&r.elem, out, allow_leaf),
89        Type::Path(tp) => {
90            let Some(seg) = tp.path.segments.last() else {
91                return;
92            };
93
94            let ident = seg.ident.to_string();
95
96            let unwrap_first_generic = |out: &mut Vec<Type>| {
97                if let PathArguments::AngleBracketed(args) = &seg.arguments {
98                    if let Some(GenericArgument::Type(inner)) = args.args.first() {
99                        extract_schema_types(inner, out, true);
100                    }
101                }
102            };
103
104            match ident.as_str() {
105                // Request/response wrappers
106                "Json" | "ValidatedJson" | "Created" => {
107                    unwrap_first_generic(out);
108                }
109                // WithStatus<T, CODE>
110                "WithStatus" => {
111                    if let PathArguments::AngleBracketed(args) = &seg.arguments {
112                        if let Some(GenericArgument::Type(inner)) = args.args.first() {
113                            extract_schema_types(inner, out, true);
114                        }
115                    }
116                }
117                // Common combinators
118                "Option" | "Result" => {
119                    if let PathArguments::AngleBracketed(args) = &seg.arguments {
120                        if let Some(GenericArgument::Type(inner)) = args.args.first() {
121                            extract_schema_types(inner, out, allow_leaf);
122                        }
123                    }
124                }
125                _ => {
126                    if allow_leaf {
127                        out.push(ty.clone());
128                    }
129                }
130            }
131        }
132        _ => {}
133    }
134}
135
136fn collect_handler_schema_types(input: &ItemFn) -> Vec<Type> {
137    let mut found: Vec<Type> = Vec::new();
138
139    for arg in &input.sig.inputs {
140        if let FnArg::Typed(pat_ty) = arg {
141            extract_schema_types(&pat_ty.ty, &mut found, false);
142        }
143    }
144
145    if let ReturnType::Type(_, ty) = &input.sig.output {
146        extract_schema_types(ty, &mut found, false);
147    }
148
149    // Dedup by token string.
150    let mut seen = HashSet::<String>::new();
151    found
152        .into_iter()
153        .filter(|t| seen.insert(quote!(#t).to_string()))
154        .collect()
155}
156
157/// Check if RUSTAPI_DEBUG is enabled at compile time
158fn is_debug_enabled() -> bool {
159    std::env::var("RUSTAPI_DEBUG")
160        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
161        .unwrap_or(false)
162}
163
164/// Print debug output if RUSTAPI_DEBUG=1 is set
165fn debug_output(name: &str, tokens: &proc_macro2::TokenStream) {
166    if is_debug_enabled() {
167        eprintln!("\n=== RUSTAPI_DEBUG: {} ===", name);
168        eprintln!("{}", tokens);
169        eprintln!("=== END {} ===\n", name);
170    }
171}
172
173/// Validate route path syntax at compile time
174///
175/// Returns Ok(()) if the path is valid, or Err with a descriptive error message.
176fn validate_path_syntax(path: &str, span: proc_macro2::Span) -> Result<(), syn::Error> {
177    // Path must start with /
178    if !path.starts_with('/') {
179        return Err(syn::Error::new(
180            span,
181            format!("route path must start with '/', got: \"{}\"", path),
182        ));
183    }
184
185    // Check for empty path segments (double slashes)
186    if path.contains("//") {
187        return Err(syn::Error::new(
188            span,
189            format!(
190                "route path contains empty segment (double slash): \"{}\"",
191                path
192            ),
193        ));
194    }
195
196    // Validate path parameter syntax
197    let mut brace_depth = 0;
198    let mut param_start = None;
199
200    for (i, ch) in path.char_indices() {
201        match ch {
202            '{' => {
203                if brace_depth > 0 {
204                    return Err(syn::Error::new(
205                        span,
206                        format!(
207                            "nested braces are not allowed in route path at position {}: \"{}\"",
208                            i, path
209                        ),
210                    ));
211                }
212                brace_depth += 1;
213                param_start = Some(i);
214            }
215            '}' => {
216                if brace_depth == 0 {
217                    return Err(syn::Error::new(
218                        span,
219                        format!(
220                            "unmatched closing brace '}}' at position {} in route path: \"{}\"",
221                            i, path
222                        ),
223                    ));
224                }
225                brace_depth -= 1;
226
227                // Check that parameter name is not empty
228                if let Some(start) = param_start {
229                    let param_name = &path[start + 1..i];
230                    if param_name.is_empty() {
231                        return Err(syn::Error::new(
232                            span,
233                            format!(
234                                "empty parameter name '{{}}' at position {} in route path: \"{}\"",
235                                start, path
236                            ),
237                        ));
238                    }
239                    // Validate parameter name contains only valid identifier characters
240                    if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
241                        return Err(syn::Error::new(
242                            span,
243                            format!(
244                                "invalid parameter name '{{{}}}' at position {} - parameter names must contain only alphanumeric characters and underscores: \"{}\"",
245                                param_name, start, path
246                            ),
247                        ));
248                    }
249                    // Parameter name must not start with a digit
250                    if param_name
251                        .chars()
252                        .next()
253                        .map(|c| c.is_ascii_digit())
254                        .unwrap_or(false)
255                    {
256                        return Err(syn::Error::new(
257                            span,
258                            format!(
259                                "parameter name '{{{}}}' cannot start with a digit at position {}: \"{}\"",
260                                param_name, start, path
261                            ),
262                        ));
263                    }
264                }
265                param_start = None;
266            }
267            // Check for invalid characters in path (outside of parameters)
268            _ if brace_depth == 0 => {
269                // Allow alphanumeric, -, _, ., /, and common URL characters
270                if !ch.is_alphanumeric() && !"-_./*".contains(ch) {
271                    return Err(syn::Error::new(
272                        span,
273                        format!(
274                            "invalid character '{}' at position {} in route path: \"{}\"",
275                            ch, i, path
276                        ),
277                    ));
278                }
279            }
280            _ => {}
281        }
282    }
283
284    // Check for unclosed braces
285    if brace_depth > 0 {
286        return Err(syn::Error::new(
287            span,
288            format!(
289                "unclosed brace '{{' in route path (missing closing '}}'): \"{}\"",
290                path
291            ),
292        ));
293    }
294
295    Ok(())
296}
297
298/// Main entry point macro for RustAPI applications
299///
300/// This macro wraps your async main function with the tokio runtime.
301///
302/// # Example
303///
304/// ```rust,ignore
305/// use rustapi_rs::prelude::*;
306///
307/// #[rustapi::main]
308/// async fn main() -> Result<()> {
309///     RustApi::new()
310///         .mount(hello)
311///         .run("127.0.0.1:8080")
312///         .await
313/// }
314/// ```
315#[proc_macro_attribute]
316pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
317    let input = parse_macro_input!(item as ItemFn);
318
319    let attrs = &input.attrs;
320    let vis = &input.vis;
321    let sig = &input.sig;
322    let block = &input.block;
323
324    let expanded = quote! {
325        #(#attrs)*
326        #[::tokio::main]
327        #vis #sig {
328            #block
329        }
330    };
331
332    debug_output("main", &expanded);
333
334    TokenStream::from(expanded)
335}
336
337/// Internal helper to generate route handler macros
338fn generate_route_handler(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
339    let path = parse_macro_input!(attr as LitStr);
340    let input = parse_macro_input!(item as ItemFn);
341
342    let fn_name = &input.sig.ident;
343    let fn_vis = &input.vis;
344    let fn_attrs = &input.attrs;
345    let fn_async = &input.sig.asyncness;
346    let fn_inputs = &input.sig.inputs;
347    let fn_output = &input.sig.output;
348    let fn_block = &input.block;
349    let fn_generics = &input.sig.generics;
350
351    let schema_types = collect_handler_schema_types(&input);
352
353    let path_value = path.value();
354
355    // Validate path syntax at compile time
356    if let Err(err) = validate_path_syntax(&path_value, path.span()) {
357        return err.to_compile_error().into();
358    }
359
360    // Generate a companion module with route info
361    let route_fn_name = syn::Ident::new(&format!("{}_route", fn_name), fn_name.span());
362    // Generate unique name for auto-registration static
363    let auto_route_name = syn::Ident::new(&format!("__AUTO_ROUTE_{}", fn_name), fn_name.span());
364
365    // Generate unique names for schema registration
366    let schema_reg_fn_name =
367        syn::Ident::new(&format!("__{}_register_schemas", fn_name), fn_name.span());
368    let auto_schema_name = syn::Ident::new(&format!("__AUTO_SCHEMA_{}", fn_name), fn_name.span());
369
370    // Pick the right route helper function based on method
371    let route_helper = match method {
372        "GET" => quote!(::rustapi_rs::get_route),
373        "POST" => quote!(::rustapi_rs::post_route),
374        "PUT" => quote!(::rustapi_rs::put_route),
375        "PATCH" => quote!(::rustapi_rs::patch_route),
376        "DELETE" => quote!(::rustapi_rs::delete_route),
377        _ => quote!(::rustapi_rs::get_route),
378    };
379
380    // Extract metadata from attributes to chain builder methods
381    let mut chained_calls = quote!();
382
383    for attr in fn_attrs {
384        // Check for tag, summary, description
385        // Use loose matching on the last segment to handle crate renaming or fully qualified paths
386        if let Some(ident) = attr.path().segments.last().map(|s| &s.ident) {
387            let ident_str = ident.to_string();
388            if ident_str == "tag" {
389                if let Ok(lit) = attr.parse_args::<LitStr>() {
390                    let val = lit.value();
391                    chained_calls = quote! { #chained_calls .tag(#val) };
392                }
393            } else if ident_str == "summary" {
394                if let Ok(lit) = attr.parse_args::<LitStr>() {
395                    let val = lit.value();
396                    chained_calls = quote! { #chained_calls .summary(#val) };
397                }
398            } else if ident_str == "description" {
399                if let Ok(lit) = attr.parse_args::<LitStr>() {
400                    let val = lit.value();
401                    chained_calls = quote! { #chained_calls .description(#val) };
402                }
403            }
404        }
405    }
406
407    let expanded = quote! {
408        // The original handler function
409        #(#fn_attrs)*
410        #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
411
412        // Route info function - creates a Route for this handler
413        #[doc(hidden)]
414        #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
415            #route_helper(#path_value, #fn_name)
416                #chained_calls
417        }
418
419        // Auto-register route with linkme
420        #[doc(hidden)]
421        #[allow(non_upper_case_globals)]
422        #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_ROUTES)]
423        #[linkme(crate = ::rustapi_rs::__private::linkme)]
424        static #auto_route_name: fn() -> ::rustapi_rs::Route = #route_fn_name;
425
426        // Auto-register referenced schemas with linkme (best-effort)
427        #[doc(hidden)]
428        #[allow(non_snake_case)]
429        fn #schema_reg_fn_name(spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) {
430            #( spec.register_in_place::<#schema_types>(); )*
431        }
432
433        #[doc(hidden)]
434        #[allow(non_upper_case_globals)]
435        #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
436        #[linkme(crate = ::rustapi_rs::__private::linkme)]
437        static #auto_schema_name: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) = #schema_reg_fn_name;
438    };
439
440    debug_output(&format!("{} {}", method, path_value), &expanded);
441
442    TokenStream::from(expanded)
443}
444
445/// GET route handler macro
446///
447/// # Example
448///
449/// ```rust,ignore
450/// #[rustapi::get("/users")]
451/// async fn list_users() -> Json<Vec<User>> {
452///     Json(vec![])
453/// }
454///
455/// #[rustapi::get("/users/{id}")]
456/// async fn get_user(Path(id): Path<i64>) -> Result<User> {
457///     Ok(User { id, name: "John".into() })
458/// }
459/// ```
460#[proc_macro_attribute]
461pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
462    generate_route_handler("GET", attr, item)
463}
464
465/// POST route handler macro
466#[proc_macro_attribute]
467pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
468    generate_route_handler("POST", attr, item)
469}
470
471/// PUT route handler macro
472#[proc_macro_attribute]
473pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
474    generate_route_handler("PUT", attr, item)
475}
476
477/// PATCH route handler macro
478#[proc_macro_attribute]
479pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
480    generate_route_handler("PATCH", attr, item)
481}
482
483/// DELETE route handler macro
484#[proc_macro_attribute]
485pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
486    generate_route_handler("DELETE", attr, item)
487}
488
489// ============================================
490// Route Metadata Macros
491// ============================================
492
493/// Tag macro for grouping endpoints in OpenAPI documentation
494///
495/// # Example
496///
497/// ```rust,ignore
498/// #[rustapi::get("/users")]
499/// #[rustapi::tag("Users")]
500/// async fn list_users() -> Json<Vec<User>> {
501///     Json(vec![])
502/// }
503/// ```
504#[proc_macro_attribute]
505pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
506    let tag = parse_macro_input!(attr as LitStr);
507    let input = parse_macro_input!(item as ItemFn);
508
509    let attrs = &input.attrs;
510    let vis = &input.vis;
511    let sig = &input.sig;
512    let block = &input.block;
513    let tag_value = tag.value();
514
515    // Add a doc comment with the tag info for documentation
516    let expanded = quote! {
517        #[doc = concat!("**Tag:** ", #tag_value)]
518        #(#attrs)*
519        #vis #sig #block
520    };
521
522    TokenStream::from(expanded)
523}
524
525/// Summary macro for endpoint summary in OpenAPI documentation
526///
527/// # Example
528///
529/// ```rust,ignore
530/// #[rustapi::get("/users")]
531/// #[rustapi::summary("List all users")]
532/// async fn list_users() -> Json<Vec<User>> {
533///     Json(vec![])
534/// }
535/// ```
536#[proc_macro_attribute]
537pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
538    let summary = parse_macro_input!(attr as LitStr);
539    let input = parse_macro_input!(item as ItemFn);
540
541    let attrs = &input.attrs;
542    let vis = &input.vis;
543    let sig = &input.sig;
544    let block = &input.block;
545    let summary_value = summary.value();
546
547    // Add a doc comment with the summary
548    let expanded = quote! {
549        #[doc = #summary_value]
550        #(#attrs)*
551        #vis #sig #block
552    };
553
554    TokenStream::from(expanded)
555}
556
557/// Description macro for detailed endpoint description in OpenAPI documentation
558///
559/// # Example
560///
561/// ```rust,ignore
562/// #[rustapi::get("/users")]
563/// #[rustapi::description("Returns a list of all users in the system. Supports pagination.")]
564/// async fn list_users() -> Json<Vec<User>> {
565///     Json(vec![])
566/// }
567/// ```
568#[proc_macro_attribute]
569pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
570    let desc = parse_macro_input!(attr as LitStr);
571    let input = parse_macro_input!(item as ItemFn);
572
573    let attrs = &input.attrs;
574    let vis = &input.vis;
575    let sig = &input.sig;
576    let block = &input.block;
577    let desc_value = desc.value();
578
579    // Add a doc comment with the description
580    let expanded = quote! {
581        #[doc = ""]
582        #[doc = #desc_value]
583        #(#attrs)*
584        #vis #sig #block
585    };
586
587    TokenStream::from(expanded)
588}
589
590// ============================================
591// Validation Derive Macro
592// ============================================
593
594/// Parsed validation rule from field attributes
595#[derive(Debug)]
596struct ValidationRuleInfo {
597    rule_type: String,
598    params: Vec<(String, String)>,
599    message: Option<String>,
600    #[allow(dead_code)]
601    group: Option<String>,
602}
603
604/// Parse validation attributes from a field
605fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
606    let mut rules = Vec::new();
607
608    for attr in attrs {
609        if !attr.path().is_ident("validate") {
610            continue;
611        }
612
613        // Parse the validate attribute
614        if let Ok(meta) = attr.parse_args::<Meta>() {
615            if let Some(rule) = parse_validate_meta(&meta) {
616                rules.push(rule);
617            }
618        } else if let Ok(nested) = attr
619            .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
620        {
621            for meta in nested {
622                if let Some(rule) = parse_validate_meta(&meta) {
623                    rules.push(rule);
624                }
625            }
626        }
627    }
628
629    rules
630}
631
632/// Parse a single validation meta item
633fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
634    match meta {
635        Meta::Path(path) => {
636            // Simple rule like #[validate(email)]
637            let ident = path.get_ident()?.to_string();
638            Some(ValidationRuleInfo {
639                rule_type: ident,
640                params: Vec::new(),
641                message: None,
642                group: None,
643            })
644        }
645        Meta::List(list) => {
646            // Rule with params like #[validate(length(min = 3, max = 50))]
647            let rule_type = list.path.get_ident()?.to_string();
648            let mut params = Vec::new();
649            let mut message = None;
650            let mut group = None;
651
652            // Parse nested params
653            if let Ok(nested) = list.parse_args_with(
654                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
655            ) {
656                for nested_meta in nested {
657                    if let Meta::NameValue(nv) = &nested_meta {
658                        let key = nv.path.get_ident()?.to_string();
659                        let value = expr_to_string(&nv.value)?;
660
661                        if key == "message" {
662                            message = Some(value);
663                        } else if key == "group" {
664                            group = Some(value);
665                        } else {
666                            params.push((key, value));
667                        }
668                    }
669                }
670            }
671
672            Some(ValidationRuleInfo {
673                rule_type,
674                params,
675                message,
676                group,
677            })
678        }
679        Meta::NameValue(nv) => {
680            // Rule like #[validate(regex = "pattern")]
681            let rule_type = nv.path.get_ident()?.to_string();
682            let value = expr_to_string(&nv.value)?;
683
684            Some(ValidationRuleInfo {
685                rule_type: rule_type.clone(),
686                params: vec![(rule_type, value)],
687                message: None,
688                group: None,
689            })
690        }
691    }
692}
693
694/// Convert an expression to a string value
695fn expr_to_string(expr: &Expr) -> Option<String> {
696    match expr {
697        Expr::Lit(lit) => match &lit.lit {
698            Lit::Str(s) => Some(s.value()),
699            Lit::Int(i) => Some(i.base10_digits().to_string()),
700            Lit::Float(f) => Some(f.base10_digits().to_string()),
701            Lit::Bool(b) => Some(b.value.to_string()),
702            _ => None,
703        },
704        _ => None,
705    }
706}
707
708/// Generate validation code for a single rule
709fn generate_rule_validation(
710    field_name: &str,
711    _field_type: &Type,
712    rule: &ValidationRuleInfo,
713) -> proc_macro2::TokenStream {
714    let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
715    let field_name_str = field_name;
716
717    match rule.rule_type.as_str() {
718        "email" => {
719            let message = rule
720                .message
721                .as_ref()
722                .map(|m| quote! { .with_message(#m) })
723                .unwrap_or_default();
724            quote! {
725                {
726                    let rule = ::rustapi_validate::v2::EmailRule::new() #message;
727                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
728                        errors.add(#field_name_str, e);
729                    }
730                }
731            }
732        }
733        "length" => {
734            let min = rule
735                .params
736                .iter()
737                .find(|(k, _)| k == "min")
738                .and_then(|(_, v)| v.parse::<usize>().ok());
739            let max = rule
740                .params
741                .iter()
742                .find(|(k, _)| k == "max")
743                .and_then(|(_, v)| v.parse::<usize>().ok());
744            let message = rule
745                .message
746                .as_ref()
747                .map(|m| quote! { .with_message(#m) })
748                .unwrap_or_default();
749
750            let rule_creation = match (min, max) {
751                (Some(min), Some(max)) => {
752                    quote! { ::rustapi_validate::v2::LengthRule::new(#min, #max) }
753                }
754                (Some(min), None) => quote! { ::rustapi_validate::v2::LengthRule::min(#min) },
755                (None, Some(max)) => quote! { ::rustapi_validate::v2::LengthRule::max(#max) },
756                (None, None) => quote! { ::rustapi_validate::v2::LengthRule::new(0, usize::MAX) },
757            };
758
759            quote! {
760                {
761                    let rule = #rule_creation #message;
762                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
763                        errors.add(#field_name_str, e);
764                    }
765                }
766            }
767        }
768        "range" => {
769            let min = rule
770                .params
771                .iter()
772                .find(|(k, _)| k == "min")
773                .map(|(_, v)| v.clone());
774            let max = rule
775                .params
776                .iter()
777                .find(|(k, _)| k == "max")
778                .map(|(_, v)| v.clone());
779            let message = rule
780                .message
781                .as_ref()
782                .map(|m| quote! { .with_message(#m) })
783                .unwrap_or_default();
784
785            // Determine the numeric type from the field type
786            let rule_creation = match (min, max) {
787                (Some(min), Some(max)) => {
788                    let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
789                    let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
790                    quote! { ::rustapi_validate::v2::RangeRule::new(#min_lit, #max_lit) }
791                }
792                (Some(min), None) => {
793                    let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
794                    quote! { ::rustapi_validate::v2::RangeRule::min(#min_lit) }
795                }
796                (None, Some(max)) => {
797                    let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
798                    quote! { ::rustapi_validate::v2::RangeRule::max(#max_lit) }
799                }
800                (None, None) => {
801                    return quote! {};
802                }
803            };
804
805            quote! {
806                {
807                    let rule = #rule_creation #message;
808                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
809                        errors.add(#field_name_str, e);
810                    }
811                }
812            }
813        }
814        "regex" => {
815            let pattern = rule
816                .params
817                .iter()
818                .find(|(k, _)| k == "regex" || k == "pattern")
819                .map(|(_, v)| v.clone())
820                .unwrap_or_default();
821            let message = rule
822                .message
823                .as_ref()
824                .map(|m| quote! { .with_message(#m) })
825                .unwrap_or_default();
826
827            quote! {
828                {
829                    let rule = ::rustapi_validate::v2::RegexRule::new(#pattern) #message;
830                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
831                        errors.add(#field_name_str, e);
832                    }
833                }
834            }
835        }
836        "url" => {
837            let message = rule
838                .message
839                .as_ref()
840                .map(|m| quote! { .with_message(#m) })
841                .unwrap_or_default();
842            quote! {
843                {
844                    let rule = ::rustapi_validate::v2::UrlRule::new() #message;
845                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
846                        errors.add(#field_name_str, e);
847                    }
848                }
849            }
850        }
851        "required" => {
852            let message = rule
853                .message
854                .as_ref()
855                .map(|m| quote! { .with_message(#m) })
856                .unwrap_or_default();
857            quote! {
858                {
859                    let rule = ::rustapi_validate::v2::RequiredRule::new() #message;
860                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
861                        errors.add(#field_name_str, e);
862                    }
863                }
864            }
865        }
866        _ => {
867            // Unknown rule - skip
868            quote! {}
869        }
870    }
871}
872
873/// Generate async validation code for a single rule
874fn generate_async_rule_validation(
875    field_name: &str,
876    rule: &ValidationRuleInfo,
877) -> proc_macro2::TokenStream {
878    let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
879    let field_name_str = field_name;
880
881    match rule.rule_type.as_str() {
882        "async_unique" => {
883            let table = rule
884                .params
885                .iter()
886                .find(|(k, _)| k == "table")
887                .map(|(_, v)| v.clone())
888                .unwrap_or_default();
889            let column = rule
890                .params
891                .iter()
892                .find(|(k, _)| k == "column")
893                .map(|(_, v)| v.clone())
894                .unwrap_or_default();
895            let message = rule
896                .message
897                .as_ref()
898                .map(|m| quote! { .with_message(#m) })
899                .unwrap_or_default();
900
901            quote! {
902                {
903                    let rule = ::rustapi_validate::v2::AsyncUniqueRule::new(#table, #column) #message;
904                    if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
905                        errors.add(#field_name_str, e);
906                    }
907                }
908            }
909        }
910        "async_exists" => {
911            let table = rule
912                .params
913                .iter()
914                .find(|(k, _)| k == "table")
915                .map(|(_, v)| v.clone())
916                .unwrap_or_default();
917            let column = rule
918                .params
919                .iter()
920                .find(|(k, _)| k == "column")
921                .map(|(_, v)| v.clone())
922                .unwrap_or_default();
923            let message = rule
924                .message
925                .as_ref()
926                .map(|m| quote! { .with_message(#m) })
927                .unwrap_or_default();
928
929            quote! {
930                {
931                    let rule = ::rustapi_validate::v2::AsyncExistsRule::new(#table, #column) #message;
932                    if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
933                        errors.add(#field_name_str, e);
934                    }
935                }
936            }
937        }
938        "async_api" => {
939            let endpoint = rule
940                .params
941                .iter()
942                .find(|(k, _)| k == "endpoint")
943                .map(|(_, v)| v.clone())
944                .unwrap_or_default();
945            let message = rule
946                .message
947                .as_ref()
948                .map(|m| quote! { .with_message(#m) })
949                .unwrap_or_default();
950
951            quote! {
952                {
953                    let rule = ::rustapi_validate::v2::AsyncApiRule::new(#endpoint) #message;
954                    if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
955                        errors.add(#field_name_str, e);
956                    }
957                }
958            }
959        }
960        _ => {
961            // Not an async rule
962            quote! {}
963        }
964    }
965}
966
967/// Check if a rule is async
968fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
969    matches!(
970        rule.rule_type.as_str(),
971        "async_unique" | "async_exists" | "async_api"
972    )
973}
974
975/// Derive macro for implementing Validate and AsyncValidate traits
976///
977/// # Example
978///
979/// ```rust,ignore
980/// use rustapi_macros::Validate;
981///
982/// #[derive(Validate)]
983/// struct CreateUser {
984///     #[validate(email, message = "Invalid email format")]
985///     email: String,
986///     
987///     #[validate(length(min = 3, max = 50))]
988///     username: String,
989///     
990///     #[validate(range(min = 18, max = 120))]
991///     age: u8,
992///     
993///     #[validate(async_unique(table = "users", column = "email"))]
994///     email: String,
995/// }
996/// ```
997#[proc_macro_derive(Validate, attributes(validate))]
998pub fn derive_validate(input: TokenStream) -> TokenStream {
999    let input = parse_macro_input!(input as DeriveInput);
1000    let name = &input.ident;
1001    let generics = &input.generics;
1002    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1003
1004    // Only support structs with named fields
1005    let fields = match &input.data {
1006        Data::Struct(data) => match &data.fields {
1007            Fields::Named(fields) => &fields.named,
1008            _ => {
1009                return syn::Error::new_spanned(
1010                    &input,
1011                    "Validate can only be derived for structs with named fields",
1012                )
1013                .to_compile_error()
1014                .into();
1015            }
1016        },
1017        _ => {
1018            return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1019                .to_compile_error()
1020                .into();
1021        }
1022    };
1023
1024    // Collect sync and async validation code for each field
1025    let mut sync_validations = Vec::new();
1026    let mut async_validations = Vec::new();
1027    let mut has_async_rules = false;
1028
1029    for field in fields {
1030        let field_name = field.ident.as_ref().unwrap().to_string();
1031        let field_type = &field.ty;
1032        let rules = parse_validate_attrs(&field.attrs);
1033
1034        for rule in &rules {
1035            if is_async_rule(rule) {
1036                has_async_rules = true;
1037                let validation = generate_async_rule_validation(&field_name, rule);
1038                async_validations.push(validation);
1039            } else {
1040                let validation = generate_rule_validation(&field_name, field_type, rule);
1041                sync_validations.push(validation);
1042            }
1043        }
1044    }
1045
1046    // Generate the Validate impl
1047    let validate_impl = quote! {
1048        impl #impl_generics ::rustapi_validate::v2::Validate for #name #ty_generics #where_clause {
1049            fn validate(&self) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1050                let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1051
1052                #(#sync_validations)*
1053
1054                errors.into_result()
1055            }
1056        }
1057    };
1058
1059    // Generate the AsyncValidate impl if there are async rules
1060    let async_validate_impl = if has_async_rules {
1061        quote! {
1062            #[::async_trait::async_trait]
1063            impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1064                async fn validate_async(&self, ctx: &::rustapi_validate::v2::ValidationContext) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1065                    let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1066
1067                    #(#async_validations)*
1068
1069                    errors.into_result()
1070                }
1071            }
1072        }
1073    } else {
1074        // Provide a default AsyncValidate impl that just returns Ok
1075        quote! {
1076            #[::async_trait::async_trait]
1077            impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1078                async fn validate_async(&self, _ctx: &::rustapi_validate::v2::ValidationContext) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1079                    Ok(())
1080                }
1081            }
1082        }
1083    };
1084
1085    let expanded = quote! {
1086        #validate_impl
1087        #async_validate_impl
1088    };
1089
1090    debug_output("Validate derive", &expanded);
1091
1092    TokenStream::from(expanded)
1093}