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
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            }
405        }
406    }
407
408    let expanded = quote! {
409        // The original handler function
410        #(#fn_attrs)*
411        #fn_vis #fn_async fn #fn_name #fn_generics (#fn_inputs) #fn_output #fn_block
412
413        // Route info function - creates a Route for this handler
414        #[doc(hidden)]
415        #fn_vis fn #route_fn_name() -> ::rustapi_rs::Route {
416            #route_helper(#path_value, #fn_name)
417                #chained_calls
418        }
419
420        // Auto-register route with linkme
421        #[doc(hidden)]
422        #[allow(non_upper_case_globals)]
423        #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_ROUTES)]
424        #[linkme(crate = ::rustapi_rs::__private::linkme)]
425        static #auto_route_name: fn() -> ::rustapi_rs::Route = #route_fn_name;
426
427        // Auto-register referenced schemas with linkme (best-effort)
428        #[doc(hidden)]
429        #[allow(non_snake_case)]
430        fn #schema_reg_fn_name(spec: &mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) {
431            #( spec.register_in_place::<#schema_types>(); )*
432        }
433
434        #[doc(hidden)]
435        #[allow(non_upper_case_globals)]
436        #[::rustapi_rs::__private::linkme::distributed_slice(::rustapi_rs::__private::AUTO_SCHEMAS)]
437        #[linkme(crate = ::rustapi_rs::__private::linkme)]
438        static #auto_schema_name: fn(&mut ::rustapi_rs::__private::rustapi_openapi::OpenApiSpec) = #schema_reg_fn_name;
439    };
440
441    debug_output(&format!("{} {}", method, path_value), &expanded);
442
443    TokenStream::from(expanded)
444}
445
446/// GET route handler macro
447///
448/// # Example
449///
450/// ```rust,ignore
451/// #[rustapi::get("/users")]
452/// async fn list_users() -> Json<Vec<User>> {
453///     Json(vec![])
454/// }
455///
456/// #[rustapi::get("/users/{id}")]
457/// async fn get_user(Path(id): Path<i64>) -> Result<User> {
458///     Ok(User { id, name: "John".into() })
459/// }
460/// ```
461#[proc_macro_attribute]
462pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
463    generate_route_handler("GET", attr, item)
464}
465
466/// POST route handler macro
467#[proc_macro_attribute]
468pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
469    generate_route_handler("POST", attr, item)
470}
471
472/// PUT route handler macro
473#[proc_macro_attribute]
474pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
475    generate_route_handler("PUT", attr, item)
476}
477
478/// PATCH route handler macro
479#[proc_macro_attribute]
480pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
481    generate_route_handler("PATCH", attr, item)
482}
483
484/// DELETE route handler macro
485#[proc_macro_attribute]
486pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
487    generate_route_handler("DELETE", attr, item)
488}
489
490// ============================================
491// Route Metadata Macros
492// ============================================
493
494/// Tag macro for grouping endpoints in OpenAPI documentation
495///
496/// # Example
497///
498/// ```rust,ignore
499/// #[rustapi::get("/users")]
500/// #[rustapi::tag("Users")]
501/// async fn list_users() -> Json<Vec<User>> {
502///     Json(vec![])
503/// }
504/// ```
505#[proc_macro_attribute]
506pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
507    let tag = parse_macro_input!(attr as LitStr);
508    let input = parse_macro_input!(item as ItemFn);
509
510    let attrs = &input.attrs;
511    let vis = &input.vis;
512    let sig = &input.sig;
513    let block = &input.block;
514    let tag_value = tag.value();
515
516    // Add a doc comment with the tag info for documentation
517    let expanded = quote! {
518        #[doc = concat!("**Tag:** ", #tag_value)]
519        #(#attrs)*
520        #vis #sig #block
521    };
522
523    TokenStream::from(expanded)
524}
525
526/// Summary macro for endpoint summary in OpenAPI documentation
527///
528/// # Example
529///
530/// ```rust,ignore
531/// #[rustapi::get("/users")]
532/// #[rustapi::summary("List all users")]
533/// async fn list_users() -> Json<Vec<User>> {
534///     Json(vec![])
535/// }
536/// ```
537#[proc_macro_attribute]
538pub fn summary(attr: TokenStream, item: TokenStream) -> TokenStream {
539    let summary = parse_macro_input!(attr as LitStr);
540    let input = parse_macro_input!(item as ItemFn);
541
542    let attrs = &input.attrs;
543    let vis = &input.vis;
544    let sig = &input.sig;
545    let block = &input.block;
546    let summary_value = summary.value();
547
548    // Add a doc comment with the summary
549    let expanded = quote! {
550        #[doc = #summary_value]
551        #(#attrs)*
552        #vis #sig #block
553    };
554
555    TokenStream::from(expanded)
556}
557
558/// Description macro for detailed endpoint description in OpenAPI documentation
559///
560/// # Example
561///
562/// ```rust,ignore
563/// #[rustapi::get("/users")]
564/// #[rustapi::description("Returns a list of all users in the system. Supports pagination.")]
565/// async fn list_users() -> Json<Vec<User>> {
566///     Json(vec![])
567/// }
568/// ```
569#[proc_macro_attribute]
570pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
571    let desc = parse_macro_input!(attr as LitStr);
572    let input = parse_macro_input!(item as ItemFn);
573
574    let attrs = &input.attrs;
575    let vis = &input.vis;
576    let sig = &input.sig;
577    let block = &input.block;
578    let desc_value = desc.value();
579
580    // Add a doc comment with the description
581    let expanded = quote! {
582        #[doc = ""]
583        #[doc = #desc_value]
584        #(#attrs)*
585        #vis #sig #block
586    };
587
588    TokenStream::from(expanded)
589}
590
591// ============================================
592// Validation Derive Macro
593// ============================================
594
595/// Parsed validation rule from field attributes
596#[derive(Debug)]
597struct ValidationRuleInfo {
598    rule_type: String,
599    params: Vec<(String, String)>,
600    message: Option<String>,
601    #[allow(dead_code)]
602    group: Option<String>,
603}
604
605/// Parse validation attributes from a field
606fn parse_validate_attrs(attrs: &[Attribute]) -> Vec<ValidationRuleInfo> {
607    let mut rules = Vec::new();
608
609    for attr in attrs {
610        if !attr.path().is_ident("validate") {
611            continue;
612        }
613
614        // Parse the validate attribute
615        if let Ok(meta) = attr.parse_args::<Meta>() {
616            if let Some(rule) = parse_validate_meta(&meta) {
617                rules.push(rule);
618            }
619        } else if let Ok(nested) = attr
620            .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
621        {
622            for meta in nested {
623                if let Some(rule) = parse_validate_meta(&meta) {
624                    rules.push(rule);
625                }
626            }
627        }
628    }
629
630    rules
631}
632
633/// Parse a single validation meta item
634fn parse_validate_meta(meta: &Meta) -> Option<ValidationRuleInfo> {
635    match meta {
636        Meta::Path(path) => {
637            // Simple rule like #[validate(email)]
638            let ident = path.get_ident()?.to_string();
639            Some(ValidationRuleInfo {
640                rule_type: ident,
641                params: Vec::new(),
642                message: None,
643                group: None,
644            })
645        }
646        Meta::List(list) => {
647            // Rule with params like #[validate(length(min = 3, max = 50))]
648            let rule_type = list.path.get_ident()?.to_string();
649            let mut params = Vec::new();
650            let mut message = None;
651            let mut group = None;
652
653            // Parse nested params
654            if let Ok(nested) = list.parse_args_with(
655                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
656            ) {
657                for nested_meta in nested {
658                    if let Meta::NameValue(nv) = &nested_meta {
659                        let key = nv.path.get_ident()?.to_string();
660                        let value = expr_to_string(&nv.value)?;
661
662                        if key == "message" {
663                            message = Some(value);
664                        } else if key == "group" {
665                            group = Some(value);
666                        } else {
667                            params.push((key, value));
668                        }
669                    }
670                }
671            }
672
673            Some(ValidationRuleInfo {
674                rule_type,
675                params,
676                message,
677                group,
678            })
679        }
680        Meta::NameValue(nv) => {
681            // Rule like #[validate(regex = "pattern")]
682            let rule_type = nv.path.get_ident()?.to_string();
683            let value = expr_to_string(&nv.value)?;
684
685            Some(ValidationRuleInfo {
686                rule_type: rule_type.clone(),
687                params: vec![(rule_type, value)],
688                message: None,
689                group: None,
690            })
691        }
692    }
693}
694
695/// Convert an expression to a string value
696fn expr_to_string(expr: &Expr) -> Option<String> {
697    match expr {
698        Expr::Lit(lit) => match &lit.lit {
699            Lit::Str(s) => Some(s.value()),
700            Lit::Int(i) => Some(i.base10_digits().to_string()),
701            Lit::Float(f) => Some(f.base10_digits().to_string()),
702            Lit::Bool(b) => Some(b.value.to_string()),
703            _ => None,
704        },
705        _ => None,
706    }
707}
708
709/// Generate validation code for a single rule
710fn generate_rule_validation(
711    field_name: &str,
712    _field_type: &Type,
713    rule: &ValidationRuleInfo,
714) -> proc_macro2::TokenStream {
715    let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
716    let field_name_str = field_name;
717
718    match rule.rule_type.as_str() {
719        "email" => {
720            let message = rule
721                .message
722                .as_ref()
723                .map(|m| quote! { .with_message(#m) })
724                .unwrap_or_default();
725            quote! {
726                {
727                    let rule = ::rustapi_validate::v2::EmailRule::new() #message;
728                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
729                        errors.add(#field_name_str, e);
730                    }
731                }
732            }
733        }
734        "length" => {
735            let min = rule
736                .params
737                .iter()
738                .find(|(k, _)| k == "min")
739                .and_then(|(_, v)| v.parse::<usize>().ok());
740            let max = rule
741                .params
742                .iter()
743                .find(|(k, _)| k == "max")
744                .and_then(|(_, v)| v.parse::<usize>().ok());
745            let message = rule
746                .message
747                .as_ref()
748                .map(|m| quote! { .with_message(#m) })
749                .unwrap_or_default();
750
751            let rule_creation = match (min, max) {
752                (Some(min), Some(max)) => {
753                    quote! { ::rustapi_validate::v2::LengthRule::new(#min, #max) }
754                }
755                (Some(min), None) => quote! { ::rustapi_validate::v2::LengthRule::min(#min) },
756                (None, Some(max)) => quote! { ::rustapi_validate::v2::LengthRule::max(#max) },
757                (None, None) => quote! { ::rustapi_validate::v2::LengthRule::new(0, usize::MAX) },
758            };
759
760            quote! {
761                {
762                    let rule = #rule_creation #message;
763                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
764                        errors.add(#field_name_str, e);
765                    }
766                }
767            }
768        }
769        "range" => {
770            let min = rule
771                .params
772                .iter()
773                .find(|(k, _)| k == "min")
774                .map(|(_, v)| v.clone());
775            let max = rule
776                .params
777                .iter()
778                .find(|(k, _)| k == "max")
779                .map(|(_, v)| v.clone());
780            let message = rule
781                .message
782                .as_ref()
783                .map(|m| quote! { .with_message(#m) })
784                .unwrap_or_default();
785
786            // Determine the numeric type from the field type
787            let rule_creation = match (min, max) {
788                (Some(min), Some(max)) => {
789                    let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
790                    let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
791                    quote! { ::rustapi_validate::v2::RangeRule::new(#min_lit, #max_lit) }
792                }
793                (Some(min), None) => {
794                    let min_lit: proc_macro2::TokenStream = min.parse().unwrap();
795                    quote! { ::rustapi_validate::v2::RangeRule::min(#min_lit) }
796                }
797                (None, Some(max)) => {
798                    let max_lit: proc_macro2::TokenStream = max.parse().unwrap();
799                    quote! { ::rustapi_validate::v2::RangeRule::max(#max_lit) }
800                }
801                (None, None) => {
802                    return quote! {};
803                }
804            };
805
806            quote! {
807                {
808                    let rule = #rule_creation #message;
809                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
810                        errors.add(#field_name_str, e);
811                    }
812                }
813            }
814        }
815        "regex" => {
816            let pattern = rule
817                .params
818                .iter()
819                .find(|(k, _)| k == "regex" || k == "pattern")
820                .map(|(_, v)| v.clone())
821                .unwrap_or_default();
822            let message = rule
823                .message
824                .as_ref()
825                .map(|m| quote! { .with_message(#m) })
826                .unwrap_or_default();
827
828            quote! {
829                {
830                    let rule = ::rustapi_validate::v2::RegexRule::new(#pattern) #message;
831                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
832                        errors.add(#field_name_str, e);
833                    }
834                }
835            }
836        }
837        "url" => {
838            let message = rule
839                .message
840                .as_ref()
841                .map(|m| quote! { .with_message(#m) })
842                .unwrap_or_default();
843            quote! {
844                {
845                    let rule = ::rustapi_validate::v2::UrlRule::new() #message;
846                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
847                        errors.add(#field_name_str, e);
848                    }
849                }
850            }
851        }
852        "required" => {
853            let message = rule
854                .message
855                .as_ref()
856                .map(|m| quote! { .with_message(#m) })
857                .unwrap_or_default();
858            quote! {
859                {
860                    let rule = ::rustapi_validate::v2::RequiredRule::new() #message;
861                    if let Err(e) = ::rustapi_validate::v2::ValidationRule::validate(&rule, &self.#field_ident) {
862                        errors.add(#field_name_str, e);
863                    }
864                }
865            }
866        }
867        _ => {
868            // Unknown rule - skip
869            quote! {}
870        }
871    }
872}
873
874/// Generate async validation code for a single rule
875fn generate_async_rule_validation(
876    field_name: &str,
877    rule: &ValidationRuleInfo,
878) -> proc_macro2::TokenStream {
879    let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
880    let field_name_str = field_name;
881
882    match rule.rule_type.as_str() {
883        "async_unique" => {
884            let table = rule
885                .params
886                .iter()
887                .find(|(k, _)| k == "table")
888                .map(|(_, v)| v.clone())
889                .unwrap_or_default();
890            let column = rule
891                .params
892                .iter()
893                .find(|(k, _)| k == "column")
894                .map(|(_, v)| v.clone())
895                .unwrap_or_default();
896            let message = rule
897                .message
898                .as_ref()
899                .map(|m| quote! { .with_message(#m) })
900                .unwrap_or_default();
901
902            quote! {
903                {
904                    let rule = ::rustapi_validate::v2::AsyncUniqueRule::new(#table, #column) #message;
905                    if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
906                        errors.add(#field_name_str, e);
907                    }
908                }
909            }
910        }
911        "async_exists" => {
912            let table = rule
913                .params
914                .iter()
915                .find(|(k, _)| k == "table")
916                .map(|(_, v)| v.clone())
917                .unwrap_or_default();
918            let column = rule
919                .params
920                .iter()
921                .find(|(k, _)| k == "column")
922                .map(|(_, v)| v.clone())
923                .unwrap_or_default();
924            let message = rule
925                .message
926                .as_ref()
927                .map(|m| quote! { .with_message(#m) })
928                .unwrap_or_default();
929
930            quote! {
931                {
932                    let rule = ::rustapi_validate::v2::AsyncExistsRule::new(#table, #column) #message;
933                    if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
934                        errors.add(#field_name_str, e);
935                    }
936                }
937            }
938        }
939        "async_api" => {
940            let endpoint = rule
941                .params
942                .iter()
943                .find(|(k, _)| k == "endpoint")
944                .map(|(_, v)| v.clone())
945                .unwrap_or_default();
946            let message = rule
947                .message
948                .as_ref()
949                .map(|m| quote! { .with_message(#m) })
950                .unwrap_or_default();
951
952            quote! {
953                {
954                    let rule = ::rustapi_validate::v2::AsyncApiRule::new(#endpoint) #message;
955                    if let Err(e) = ::rustapi_validate::v2::AsyncValidationRule::validate_async(&rule, &self.#field_ident, ctx).await {
956                        errors.add(#field_name_str, e);
957                    }
958                }
959            }
960        }
961        _ => {
962            // Not an async rule
963            quote! {}
964        }
965    }
966}
967
968/// Check if a rule is async
969fn is_async_rule(rule: &ValidationRuleInfo) -> bool {
970    matches!(
971        rule.rule_type.as_str(),
972        "async_unique" | "async_exists" | "async_api"
973    )
974}
975
976/// Derive macro for implementing Validate and AsyncValidate traits
977///
978/// # Example
979///
980/// ```rust,ignore
981/// use rustapi_macros::Validate;
982///
983/// #[derive(Validate)]
984/// struct CreateUser {
985///     #[validate(email, message = "Invalid email format")]
986///     email: String,
987///     
988///     #[validate(length(min = 3, max = 50))]
989///     username: String,
990///     
991///     #[validate(range(min = 18, max = 120))]
992///     age: u8,
993///     
994///     #[validate(async_unique(table = "users", column = "email"))]
995///     email: String,
996/// }
997/// ```
998#[proc_macro_derive(Validate, attributes(validate))]
999pub fn derive_validate(input: TokenStream) -> TokenStream {
1000    let input = parse_macro_input!(input as DeriveInput);
1001    let name = &input.ident;
1002    let generics = &input.generics;
1003    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1004
1005    // Only support structs with named fields
1006    let fields = match &input.data {
1007        Data::Struct(data) => match &data.fields {
1008            Fields::Named(fields) => &fields.named,
1009            _ => {
1010                return syn::Error::new_spanned(
1011                    &input,
1012                    "Validate can only be derived for structs with named fields",
1013                )
1014                .to_compile_error()
1015                .into();
1016            }
1017        },
1018        _ => {
1019            return syn::Error::new_spanned(&input, "Validate can only be derived for structs")
1020                .to_compile_error()
1021                .into();
1022        }
1023    };
1024
1025    // Collect sync and async validation code for each field
1026    let mut sync_validations = Vec::new();
1027    let mut async_validations = Vec::new();
1028    let mut has_async_rules = false;
1029
1030    for field in fields {
1031        let field_name = field.ident.as_ref().unwrap().to_string();
1032        let field_type = &field.ty;
1033        let rules = parse_validate_attrs(&field.attrs);
1034
1035        for rule in &rules {
1036            if is_async_rule(rule) {
1037                has_async_rules = true;
1038                let validation = generate_async_rule_validation(&field_name, rule);
1039                async_validations.push(validation);
1040            } else {
1041                let validation = generate_rule_validation(&field_name, field_type, rule);
1042                sync_validations.push(validation);
1043            }
1044        }
1045    }
1046
1047    // Generate the Validate impl
1048    let validate_impl = quote! {
1049        impl #impl_generics ::rustapi_validate::v2::Validate for #name #ty_generics #where_clause {
1050            fn validate(&self) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1051                let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1052
1053                #(#sync_validations)*
1054
1055                errors.into_result()
1056            }
1057        }
1058    };
1059
1060    // Generate the AsyncValidate impl if there are async rules
1061    let async_validate_impl = if has_async_rules {
1062        quote! {
1063            #[::async_trait::async_trait]
1064            impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1065                async fn validate_async(&self, ctx: &::rustapi_validate::v2::ValidationContext) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1066                    let mut errors = ::rustapi_validate::v2::ValidationErrors::new();
1067
1068                    #(#async_validations)*
1069
1070                    errors.into_result()
1071                }
1072            }
1073        }
1074    } else {
1075        // Provide a default AsyncValidate impl that just returns Ok
1076        quote! {
1077            #[::async_trait::async_trait]
1078            impl #impl_generics ::rustapi_validate::v2::AsyncValidate for #name #ty_generics #where_clause {
1079                async fn validate_async(&self, _ctx: &::rustapi_validate::v2::ValidationContext) -> Result<(), ::rustapi_validate::v2::ValidationErrors> {
1080                    Ok(())
1081                }
1082            }
1083        }
1084    };
1085
1086    let expanded = quote! {
1087        #validate_impl
1088        #async_validate_impl
1089    };
1090
1091    debug_output("Validate derive", &expanded);
1092
1093    TokenStream::from(expanded)
1094}
1095
1096// ============================================
1097// ApiError Derive Macro
1098// ============================================
1099
1100/// Derive macro for implementing IntoResponse for error enums
1101///
1102/// # Example
1103///
1104/// ```rust,ignore
1105/// #[derive(ApiError)]
1106/// enum UserError {
1107///     #[error(status = 404, message = "User not found")]
1108///     NotFound(i64),
1109///     
1110///     #[error(status = 400, code = "validation_error")]
1111///     InvalidInput(String),
1112/// }
1113/// ```
1114#[proc_macro_derive(ApiError, attributes(error))]
1115pub fn derive_api_error(input: TokenStream) -> TokenStream {
1116    api_error::expand_derive_api_error(input)
1117}
1118
1119// ============================================
1120// TypedPath Derive Macro
1121// ============================================
1122
1123/// Derive macro for TypedPath
1124///
1125/// # Example
1126///
1127/// ```rust,ignore
1128/// #[derive(TypedPath, Deserialize, Serialize)]
1129/// #[typed_path("/users/{id}/posts/{post_id}")]
1130/// struct PostPath {
1131///     id: u64,
1132///     post_id: String,
1133/// }
1134/// ```
1135#[proc_macro_derive(TypedPath, attributes(typed_path))]
1136pub fn derive_typed_path(input: TokenStream) -> TokenStream {
1137    let input = parse_macro_input!(input as DeriveInput);
1138    let name = &input.ident;
1139    let generics = &input.generics;
1140    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1141
1142    // Find the #[typed_path("...")] attribute
1143    let mut path_str = None;
1144    for attr in &input.attrs {
1145        if attr.path().is_ident("typed_path") {
1146            if let Ok(lit) = attr.parse_args::<LitStr>() {
1147                path_str = Some(lit.value());
1148            }
1149        }
1150    }
1151
1152    let path = match path_str {
1153        Some(p) => p,
1154        None => {
1155            return syn::Error::new_spanned(
1156                &input,
1157                "#[derive(TypedPath)] requires a #[typed_path(\"...\")] attribute",
1158            )
1159            .to_compile_error()
1160            .into();
1161        }
1162    };
1163
1164    // Validate path syntax
1165    if let Err(err) = validate_path_syntax(&path, proc_macro2::Span::call_site()) {
1166        return err.to_compile_error().into();
1167    }
1168
1169    // Generate to_uri implementation
1170    // We need to parse the path and replace {param} with self.param
1171    let mut format_string = String::new();
1172    let mut format_args = Vec::new();
1173
1174    let mut chars = path.chars().peekable();
1175    while let Some(ch) = chars.next() {
1176        if ch == '{' {
1177            let mut param_name = String::new();
1178            while let Some(&c) = chars.peek() {
1179                if c == '}' {
1180                    chars.next(); // Consume '}'
1181                    break;
1182                }
1183                param_name.push(chars.next().unwrap());
1184            }
1185
1186            if param_name.is_empty() {
1187                return syn::Error::new_spanned(
1188                    &input,
1189                    "Empty path parameter not allowed in typed_path",
1190                )
1191                .to_compile_error()
1192                .into();
1193            }
1194
1195            format_string.push_str("{}");
1196            let ident = syn::Ident::new(&param_name, proc_macro2::Span::call_site());
1197            format_args.push(quote! { self.#ident });
1198        } else {
1199            format_string.push(ch);
1200        }
1201    }
1202
1203    let expanded = quote! {
1204        impl #impl_generics ::rustapi_rs::prelude::TypedPath for #name #ty_generics #where_clause {
1205            const PATH: &'static str = #path;
1206
1207            fn to_uri(&self) -> String {
1208                format!(#format_string, #(#format_args),*)
1209            }
1210        }
1211    };
1212
1213    debug_output("TypedPath derive", &expanded);
1214    TokenStream::from(expanded)
1215}