Skip to main content

rustapi_macros/
lib.rs

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