Skip to main content

api_macros/
lib.rs

1use darling::FromMeta;
2use proc_macro::TokenStream;
3use proc_macro2::TokenStream as TokenStream2;
4use quote::{format_ident, quote};
5use syn::{
6    parse_macro_input, Attribute, FnArg, ImplItem, ImplItemFn, ItemImpl, Pat, PatType, ReturnType,
7    Type,
8};
9
10/// Parsed representation of `#[api_handler(method = "POST", path = "/users/{id}")]`
11#[derive(Debug, FromMeta)]
12struct ApiHandlerArgs {
13    method: String,
14    path: String,
15}
16
17/// Parsed parameter info extracted from a handler function signature.
18#[derive(Debug)]
19struct HandlerParam {
20    name: syn::Ident,
21    ty: Box<Type>,
22    kind: ParamKind,
23}
24
25#[derive(Debug, Clone, PartialEq)]
26enum ParamKind {
27    Body,
28    Path,
29    Query,
30}
31
32/// Represents a fully parsed handler method.
33struct ParsedHandler {
34    fn_name: syn::Ident,
35    method: String,
36    path: String,
37    params: Vec<HandlerParam>,
38    return_type: Box<Type>,
39    // The original method (with &self and annotations stripped) to keep in the impl block
40    clean_method: ImplItemFn,
41    visibility: syn::Visibility,
42}
43
44/// Check if an attribute list contains a specific helper attribute like `#[body]`, `#[path]`, or `#[query]`.
45fn take_param_attr(attrs: &[Attribute]) -> Option<ParamKind> {
46    for attr in attrs {
47        if attr.path().is_ident("body") {
48            return Some(ParamKind::Body);
49        }
50        if attr.path().is_ident("path") {
51            return Some(ParamKind::Path);
52        }
53        if attr.path().is_ident("query") {
54            return Some(ParamKind::Query);
55        }
56    }
57    None
58}
59
60/// Strip `#[body]`, `#[path]`, `#[query]`, and `#[api_handler(...)]` attributes so they don't
61/// confuse the compiler in the output.
62fn strip_helper_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
63    attrs
64        .iter()
65        .filter(|a| {
66            !a.path().is_ident("body")
67                && !a.path().is_ident("path")
68                && !a.path().is_ident("query")
69        })
70        .cloned()
71        .collect()
72}
73
74fn strip_api_handler_attr(attrs: &[Attribute]) -> Vec<Attribute> {
75    attrs
76        .iter()
77        .filter(|a| !a.path().is_ident("api_handler"))
78        .cloned()
79        .collect()
80}
81
82/// Extract the inner type `T` from `MyAppResult<T>` (or any single-generic wrapper).
83fn extract_inner_type(ty: &Type) -> &Type {
84    if let Type::Path(type_path) = ty {
85        if let Some(segment) = type_path.path.segments.last() {
86            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
87                if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
88                    return inner;
89                }
90            }
91        }
92    }
93    ty
94}
95
96/// Build the path string for axum routes.
97/// Axum 0.8+ uses `{name}` syntax for path parameters, which matches our input format.
98fn path_to_axum(path: &str) -> &str {
99    path
100}
101
102fn parse_handler(method: &ImplItemFn) -> Option<ParsedHandler> {
103    // Look for #[api_handler(...)] attribute
104    let api_attr = method
105        .attrs
106        .iter()
107        .find(|a| a.path().is_ident("api_handler"))?;
108
109    let meta = &api_attr.meta;
110    let args = ApiHandlerArgs::from_meta(&meta).expect("Failed to parse #[api_handler] arguments");
111
112    let fn_name = method.sig.ident.clone();
113    let visibility = method.vis.clone();
114
115    // Parse parameters (skip &self)
116    let mut params = Vec::new();
117    for arg in method.sig.inputs.iter().skip(1) {
118        if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = arg {
119            if let Pat::Ident(pat_ident) = pat.as_ref() {
120                let kind = take_param_attr(attrs).unwrap_or_else(|| {
121                    panic!(
122                        "Parameter `{}` in `{}` must be annotated with #[body], #[path], or #[query]",
123                        pat_ident.ident, fn_name
124                    )
125                });
126                params.push(HandlerParam {
127                    name: pat_ident.ident.clone(),
128                    ty: ty.clone(),
129                    kind,
130                });
131            }
132        }
133    }
134
135    // Extract return type
136    let return_type = match &method.sig.output {
137        ReturnType::Type(_, ty) => ty.clone(),
138        ReturnType::Default => panic!("Handler `{}` must have a return type", fn_name),
139    };
140
141    // Build a clean version of the method: strip helper attrs from params and method
142    let mut clean_method = method.clone();
143    clean_method.attrs = strip_api_handler_attr(&clean_method.attrs);
144
145    // Strip #[body] / #[path] from params in the clean method
146    for arg in clean_method.sig.inputs.iter_mut() {
147        if let FnArg::Typed(pat_type) = arg {
148            pat_type.attrs = strip_helper_attrs(&pat_type.attrs);
149        }
150    }
151
152    Some(ParsedHandler {
153        fn_name,
154        method: args.method.to_uppercase(),
155        path: args.path,
156        params,
157        return_type,
158        clean_method,
159        visibility,
160    })
161}
162
163/// Generate the free-standing axum handler function for a parsed handler.
164fn generate_axum_handler(struct_name: &syn::Ident, handler: &ParsedHandler) -> TokenStream2 {
165    let fn_name = &handler.fn_name;
166    let handler_fn_name = format_ident!("__axum_handler_{}", fn_name);
167
168    // Build extractor arguments and call arguments
169    let mut extractor_params: Vec<TokenStream2> = Vec::new();
170    let mut call_args: Vec<TokenStream2> = Vec::new();
171
172    // State is always first (before body, which must be last in axum)
173    extractor_params.push(quote! {
174        axum::extract::State(state): axum::extract::State<std::sync::Arc<#struct_name>>
175    });
176
177    // Path params come before query and body
178    let path_params: Vec<_> = handler
179        .params
180        .iter()
181        .filter(|p| p.kind == ParamKind::Path)
182        .collect();
183
184    if path_params.len() == 1 {
185        let name = &path_params[0].name;
186        let ty = &path_params[0].ty;
187        extractor_params.push(quote! {
188            axum::extract::Path(#name): axum::extract::Path<#ty>
189        });
190        call_args.push(quote! { #name });
191    } else if path_params.len() > 1 {
192        // Multiple path params → extract as tuple
193        let names: Vec<_> = path_params.iter().map(|p| &p.name).collect();
194        let types: Vec<_> = path_params.iter().map(|p| &p.ty).collect();
195        extractor_params.push(quote! {
196            axum::extract::Path((#(#names),*)): axum::extract::Path<(#(#types),*)>
197        });
198        for name in &names {
199            call_args.push(quote! { #name });
200        }
201    }
202
203    // Query params come before body
204    for param in handler.params.iter().filter(|p| p.kind == ParamKind::Query) {
205        let name = &param.name;
206        let ty = &param.ty;
207        extractor_params.push(quote! {
208            axum::extract::Query(#name): axum::extract::Query<#ty>
209        });
210        call_args.push(quote! { #name });
211    }
212
213    // Body param (must be last for axum)
214    for param in handler.params.iter().filter(|p| p.kind == ParamKind::Body) {
215        let name = &param.name;
216        let ty = &param.ty;
217        extractor_params.push(quote! {
218            axum::extract::Json(#name): axum::extract::Json<#ty>
219        });
220        call_args.push(quote! { #name });
221    }
222
223    quote! {
224        async fn #handler_fn_name(
225            #(#extractor_params),*
226        ) -> impl axum::response::IntoResponse {
227            state.#fn_name(#(#call_args),*).await
228        }
229    }
230}
231
232/// Generate the `fn router()` method that wires all handlers to their routes.
233fn generate_router(_struct_name: &syn::Ident, handlers: &[ParsedHandler]) -> TokenStream2 {
234    let mut route_calls: Vec<TokenStream2> = Vec::new();
235
236    for handler in handlers {
237        let handler_fn_name = format_ident!("__axum_handler_{}", handler.fn_name);
238        let axum_path = path_to_axum(&handler.path);
239
240        let method_fn = match handler.method.as_str() {
241            "GET" => quote! { axum::routing::get },
242            "POST" => quote! { axum::routing::post },
243            "PUT" => quote! { axum::routing::put },
244            "DELETE" => quote! { axum::routing::delete },
245            "PATCH" => quote! { axum::routing::patch },
246            other => panic!("Unsupported HTTP method: {}", other),
247        };
248
249        route_calls.push(quote! {
250            .route(#axum_path, #method_fn(#handler_fn_name))
251        });
252    }
253
254    quote! {
255        /// Build an axum Router with all annotated handlers wired up.
256        pub fn router(self: std::sync::Arc<Self>) -> axum::Router {
257            axum::Router::new()
258                #(#route_calls)*
259                .with_state(self)
260        }
261    }
262}
263
264/// Generate the client struct and its impl block.
265fn generate_client(struct_name: &syn::Ident, handlers: &[ParsedHandler]) -> TokenStream2 {
266    let client_name = format_ident!("{}Client", struct_name);
267    let error_name = format_ident!("{}ClientError", struct_name);
268
269    let mut client_methods: Vec<TokenStream2> = Vec::new();
270
271    for handler in handlers {
272        let fn_name = &handler.fn_name;
273        let vis = &handler.visibility;
274        let inner_return_type = extract_inner_type(&handler.return_type);
275
276        // Build function parameters (no &self yet, we add it)
277        let mut fn_params: Vec<TokenStream2> = Vec::new();
278        let mut path_format_args: Vec<TokenStream2> = Vec::new();
279        let mut body_arg: Option<TokenStream2> = None;
280        let mut query_arg: Option<TokenStream2> = None;
281
282        for param in &handler.params {
283            let name = &param.name;
284            let ty = &param.ty;
285            match param.kind {
286                ParamKind::Path => {
287                    fn_params.push(quote! { #name: #ty });
288                    path_format_args.push(quote! { #name = #name });
289                }
290                ParamKind::Body => {
291                    fn_params.push(quote! { #name: &#ty });
292                    body_arg = Some(quote! { #name });
293                }
294                ParamKind::Query => {
295                    fn_params.push(quote! { #name: &#ty });
296                    query_arg = Some(quote! { #name });
297                }
298            }
299        }
300
301        // Build the URL expression
302        let path_str = &handler.path;
303        let base_url_expr = if path_format_args.is_empty() {
304            quote! { format!("{}{}", self.base_url, #path_str) }
305        } else {
306            quote! { format!(concat!("{}", #path_str), self.base_url, #(#path_format_args),*) }
307        };
308
309        // Add query string if there's a query param
310        let url_expr = if let Some(query) = &query_arg {
311            quote! {
312                {
313                    let base = #base_url_expr;
314                    let query_string = serde_urlencoded::to_string(#query)
315                        .expect("failed to serialize query parameters");
316                    if query_string.is_empty() {
317                        base
318                    } else {
319                        format!("{}?{}", base, query_string)
320                    }
321                }
322            }
323        } else {
324            base_url_expr
325        };
326
327        // Build the request chain
328        let method_lower = handler.method.to_lowercase();
329        let method_ident = format_ident!("{}", method_lower);
330
331        let url_ident = format_ident!("url");
332        let request_chain = if let Some(body) = &body_arg {
333            quote! {
334                self.client
335                    .#method_ident(&#url_ident)
336                    .json(#body)
337                    .send()
338                    .await
339            }
340        } else {
341            quote! {
342                self.client
343                    .#method_ident(&#url_ident)
344                    .send()
345                    .await
346            }
347        };
348
349        client_methods.push(quote! {
350            #vis async fn #fn_name(&self, #(#fn_params),*) -> Result<#inner_return_type, #error_name> {
351                let url = #url_expr;
352                let response = #request_chain
353                    .map_err(#error_name::Request)?;
354
355                if !response.status().is_success() {
356                    let status = response.status();
357                    let body = response.text().await.unwrap_or_default();
358                    return Err(#error_name::Api { status, body });
359                }
360
361                response
362                    .json::<#inner_return_type>()
363                    .await
364                    .map_err(#error_name::Request)
365            }
366        });
367    }
368
369    quote! {
370        /// Auto-generated HTTP client for the API.
371        pub struct #client_name {
372            base_url: String,
373            client: reqwest::Client,
374        }
375
376        #[derive(Debug)]
377        pub enum #error_name {
378            Request(reqwest::Error),
379            Api {
380                status: reqwest::StatusCode,
381                body: String,
382            },
383        }
384
385        impl std::fmt::Display for #error_name {
386            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387                match self {
388                    Self::Request(e) => write!(f, "HTTP request error: {e}"),
389                    Self::Api { status, body } => write!(f, "API error ({status}): {body}"),
390                }
391            }
392        }
393
394        impl std::error::Error for #error_name {
395            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
396                match self {
397                    Self::Request(e) => Some(e),
398                    Self::Api { .. } => None,
399                }
400            }
401        }
402
403        impl #client_name {
404            /// Create a new client pointing at the given base URL (e.g. `"http://localhost:3000"`).
405            pub fn new(base_url: impl Into<String>) -> Self {
406                Self {
407                    base_url: base_url.into(),
408                    client: reqwest::Client::new(),
409                }
410            }
411
412            /// Create a new client with a custom `reqwest::Client`.
413            pub fn with_client(base_url: impl Into<String>, client: reqwest::Client) -> Self {
414                Self {
415                    base_url: base_url.into(),
416                    client,
417                }
418            }
419
420            #(#client_methods)*
421        }
422    }
423}
424
425/// The main attribute macro: `#[api]`
426///
427/// Place this on an `impl` block. Methods annotated with `#[api_handler(...)]`
428/// will have axum handler functions and a typed HTTP client generated automatically.
429#[proc_macro_attribute]
430pub fn api(_attr: TokenStream, item: TokenStream) -> TokenStream {
431    let mut input = parse_macro_input!(item as ItemImpl);
432
433    // Get the struct name
434    let struct_name = if let Type::Path(type_path) = input.self_ty.as_ref() {
435        type_path
436            .path
437            .segments
438            .last()
439            .expect("Expected a struct name")
440            .ident
441            .clone()
442    } else {
443        panic!("#[api] must be applied to an impl block for a named struct");
444    };
445
446    // Parse all handler methods
447    let mut handlers: Vec<ParsedHandler> = Vec::new();
448    let mut cleaned_items: Vec<ImplItem> = Vec::new();
449
450    for item in &input.items {
451        if let ImplItem::Fn(method) = item {
452            if let Some(parsed) = parse_handler(method) {
453                cleaned_items.push(ImplItem::Fn(parsed.clean_method.clone()));
454                handlers.push(parsed);
455            } else {
456                // Not an api_handler method — keep as-is
457                cleaned_items.push(item.clone());
458            }
459        } else {
460            cleaned_items.push(item.clone());
461        }
462    }
463
464    input.items = cleaned_items;
465
466    // Generate axum handler functions
467    let axum_handlers: Vec<TokenStream2> = handlers
468        .iter()
469        .map(|h| generate_axum_handler(&struct_name, h))
470        .collect();
471
472    // Generate router method
473    let router_impl = generate_router(&struct_name, &handlers);
474
475    // Generate client
476    let client = generate_client(&struct_name, &handlers);
477
478    let expanded = quote! {
479        #input
480
481        impl #struct_name {
482            #router_impl
483        }
484
485        // Free-standing handler functions (module-private)
486        #(#axum_handlers)*
487
488        #client
489    };
490
491    TokenStream::from(expanded)
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use syn::parse_quote;
498
499    // ── Tests for take_param_attr() ─────────────────────────────────────────
500
501    #[test]
502    fn take_param_attr_body() {
503        let attrs: Vec<Attribute> = vec![parse_quote!(#[body])];
504        assert_eq!(take_param_attr(&attrs), Some(ParamKind::Body));
505    }
506
507    #[test]
508    fn take_param_attr_path() {
509        let attrs: Vec<Attribute> = vec![parse_quote!(#[path])];
510        assert_eq!(take_param_attr(&attrs), Some(ParamKind::Path));
511    }
512
513    #[test]
514    fn take_param_attr_unrelated() {
515        let attrs: Vec<Attribute> = vec![parse_quote!(#[serde(rename = "foo")])];
516        assert_eq!(take_param_attr(&attrs), None);
517    }
518
519    #[test]
520    fn take_param_attr_body_with_others() {
521        let attrs: Vec<Attribute> = vec![
522            parse_quote!(#[doc = "some doc"]),
523            parse_quote!(#[body]),
524            parse_quote!(#[allow(unused)]),
525        ];
526        assert_eq!(take_param_attr(&attrs), Some(ParamKind::Body));
527    }
528
529    #[test]
530    fn take_param_attr_path_with_others() {
531        let attrs: Vec<Attribute> = vec![parse_quote!(#[doc = "some doc"]), parse_quote!(#[path])];
532        assert_eq!(take_param_attr(&attrs), Some(ParamKind::Path));
533    }
534
535    #[test]
536    fn take_param_attr_empty() {
537        let attrs: Vec<Attribute> = vec![];
538        assert_eq!(take_param_attr(&attrs), None);
539    }
540
541    #[test]
542    fn take_param_attr_query() {
543        let attrs: Vec<Attribute> = vec![parse_quote!(#[query])];
544        assert_eq!(take_param_attr(&attrs), Some(ParamKind::Query));
545    }
546
547    #[test]
548    fn take_param_attr_query_with_others() {
549        let attrs: Vec<Attribute> = vec![parse_quote!(#[doc = "some doc"]), parse_quote!(#[query])];
550        assert_eq!(take_param_attr(&attrs), Some(ParamKind::Query));
551    }
552
553    // ── Tests for strip_helper_attrs() ──────────────────────────────────────
554
555    #[test]
556    fn strip_helper_attrs_removes_body() {
557        let attrs: Vec<Attribute> = vec![parse_quote!(#[body]), parse_quote!(#[doc = "kept"])];
558        let stripped = strip_helper_attrs(&attrs);
559        assert_eq!(stripped.len(), 1);
560        assert!(stripped[0].path().is_ident("doc"));
561    }
562
563    #[test]
564    fn strip_helper_attrs_removes_path() {
565        let attrs: Vec<Attribute> = vec![parse_quote!(#[path]), parse_quote!(#[allow(unused)])];
566        let stripped = strip_helper_attrs(&attrs);
567        assert_eq!(stripped.len(), 1);
568        assert!(stripped[0].path().is_ident("allow"));
569    }
570
571    #[test]
572    fn strip_helper_attrs_removes_both() {
573        let attrs: Vec<Attribute> = vec![
574            parse_quote!(#[body]),
575            parse_quote!(#[path]),
576            parse_quote!(#[doc = "kept"]),
577        ];
578        let stripped = strip_helper_attrs(&attrs);
579        assert_eq!(stripped.len(), 1);
580        assert!(stripped[0].path().is_ident("doc"));
581    }
582
583    #[test]
584    fn strip_helper_attrs_removes_query() {
585        let attrs: Vec<Attribute> = vec![parse_quote!(#[query]), parse_quote!(#[doc = "kept"])];
586        let stripped = strip_helper_attrs(&attrs);
587        assert_eq!(stripped.len(), 1);
588        assert!(stripped[0].path().is_ident("doc"));
589    }
590
591    #[test]
592    fn strip_helper_attrs_removes_all_three() {
593        let attrs: Vec<Attribute> = vec![
594            parse_quote!(#[body]),
595            parse_quote!(#[path]),
596            parse_quote!(#[query]),
597            parse_quote!(#[doc = "kept"]),
598        ];
599        let stripped = strip_helper_attrs(&attrs);
600        assert_eq!(stripped.len(), 1);
601        assert!(stripped[0].path().is_ident("doc"));
602    }
603
604    #[test]
605    fn strip_helper_attrs_keeps_unrelated() {
606        let attrs: Vec<Attribute> = vec![
607            parse_quote!(#[serde(rename = "foo")]),
608            parse_quote!(#[allow(unused)]),
609        ];
610        let stripped = strip_helper_attrs(&attrs);
611        assert_eq!(stripped.len(), 2);
612    }
613
614    #[test]
615    fn strip_helper_attrs_empty() {
616        let attrs: Vec<Attribute> = vec![];
617        let stripped = strip_helper_attrs(&attrs);
618        assert!(stripped.is_empty());
619    }
620
621    // ── Tests for strip_api_handler_attr() ──────────────────────────────────
622
623    #[test]
624    fn strip_api_handler_attr_removes_it() {
625        let attrs: Vec<Attribute> = vec![
626            parse_quote!(#[api_handler(method = "GET", path = "/foo")]),
627            parse_quote!(#[doc = "kept"]),
628        ];
629        let stripped = strip_api_handler_attr(&attrs);
630        assert_eq!(stripped.len(), 1);
631        assert!(stripped[0].path().is_ident("doc"));
632    }
633
634    #[test]
635    fn strip_api_handler_attr_keeps_others() {
636        let attrs: Vec<Attribute> = vec![
637            parse_quote!(#[doc = "some doc"]),
638            parse_quote!(#[allow(unused)]),
639        ];
640        let stripped = strip_api_handler_attr(&attrs);
641        assert_eq!(stripped.len(), 2);
642    }
643
644    // ── Tests for extract_inner_type() ──────────────────────────────────────
645
646    #[test]
647    fn extract_inner_type_result() {
648        let ty: Type = parse_quote!(MyAppResult<User>);
649        let inner = extract_inner_type(&ty);
650        let expected: Type = parse_quote!(User);
651        assert_eq!(quote!(#inner).to_string(), quote!(#expected).to_string());
652    }
653
654    #[test]
655    fn extract_inner_type_option() {
656        let ty: Type = parse_quote!(Option<String>);
657        let inner = extract_inner_type(&ty);
658        let expected: Type = parse_quote!(String);
659        assert_eq!(quote!(#inner).to_string(), quote!(#expected).to_string());
660    }
661
662    #[test]
663    fn extract_inner_type_nested() {
664        let ty: Type = parse_quote!(Result<Option<User>, Error>);
665        let inner = extract_inner_type(&ty);
666        // Should extract the first generic arg, which is Option<User>
667        let expected: Type = parse_quote!(Option<User>);
668        assert_eq!(quote!(#inner).to_string(), quote!(#expected).to_string());
669    }
670
671    #[test]
672    fn extract_inner_type_no_generic() {
673        let ty: Type = parse_quote!(String);
674        let inner = extract_inner_type(&ty);
675        let expected: Type = parse_quote!(String);
676        assert_eq!(quote!(#inner).to_string(), quote!(#expected).to_string());
677    }
678
679    #[test]
680    fn extract_inner_type_unit() {
681        let ty: Type = parse_quote!(MyAppResult<()>);
682        let inner = extract_inner_type(&ty);
683        let expected: Type = parse_quote!(());
684        assert_eq!(quote!(#inner).to_string(), quote!(#expected).to_string());
685    }
686
687    // ── Tests for path_to_axum() ────────────────────────────────────────────
688
689    #[test]
690    fn path_to_axum_simple() {
691        assert_eq!(path_to_axum("/users"), "/users");
692    }
693
694    #[test]
695    fn path_to_axum_with_param() {
696        assert_eq!(path_to_axum("/users/{id}"), "/users/{id}");
697    }
698
699    #[test]
700    fn path_to_axum_with_multiple_params() {
701        assert_eq!(
702            path_to_axum("/users/{user_id}/posts/{post_id}"),
703            "/users/{user_id}/posts/{post_id}"
704        );
705    }
706
707    // ── Tests for parse_handler() ───────────────────────────────────────────
708
709    #[test]
710    fn parse_handler_post_with_body() {
711        let method: ImplItemFn = parse_quote! {
712            #[api_handler(method = "POST", path = "/users")]
713            pub async fn create_user(&self, #[body] req: CreateUserRequest) -> MyAppResult<CreateUserResponse> {
714                todo!()
715            }
716        };
717
718        let parsed = parse_handler(&method).expect("Should parse successfully");
719        assert_eq!(parsed.fn_name.to_string(), "create_user");
720        assert_eq!(parsed.method, "POST");
721        assert_eq!(parsed.path, "/users");
722        assert_eq!(parsed.params.len(), 1);
723        assert_eq!(parsed.params[0].name.to_string(), "req");
724        assert_eq!(parsed.params[0].kind, ParamKind::Body);
725    }
726
727    #[test]
728    fn parse_handler_get_with_path() {
729        let method: ImplItemFn = parse_quote! {
730            #[api_handler(method = "GET", path = "/users/{id}")]
731            pub async fn get_user(&self, #[path] id: UserId) -> MyAppResult<GetUserResponse> {
732                todo!()
733            }
734        };
735
736        let parsed = parse_handler(&method).expect("Should parse successfully");
737        assert_eq!(parsed.fn_name.to_string(), "get_user");
738        assert_eq!(parsed.method, "GET");
739        assert_eq!(parsed.path, "/users/{id}");
740        assert_eq!(parsed.params.len(), 1);
741        assert_eq!(parsed.params[0].name.to_string(), "id");
742        assert_eq!(parsed.params[0].kind, ParamKind::Path);
743    }
744
745    #[test]
746    fn parse_handler_put_with_path_and_body() {
747        let method: ImplItemFn = parse_quote! {
748            #[api_handler(method = "PUT", path = "/users/{id}")]
749            pub async fn update_user(&self, #[path] id: UserId, #[body] req: UpdateUserRequest) -> MyAppResult<UpdateUserResponse> {
750                todo!()
751            }
752        };
753
754        let parsed = parse_handler(&method).expect("Should parse successfully");
755        assert_eq!(parsed.fn_name.to_string(), "update_user");
756        assert_eq!(parsed.method, "PUT");
757        assert_eq!(parsed.path, "/users/{id}");
758        assert_eq!(parsed.params.len(), 2);
759        assert_eq!(parsed.params[0].name.to_string(), "id");
760        assert_eq!(parsed.params[0].kind, ParamKind::Path);
761        assert_eq!(parsed.params[1].name.to_string(), "req");
762        assert_eq!(parsed.params[1].kind, ParamKind::Body);
763    }
764
765    #[test]
766    fn parse_handler_delete() {
767        let method: ImplItemFn = parse_quote! {
768            #[api_handler(method = "DELETE", path = "/users/{id}")]
769            pub async fn delete_user(&self, #[path] id: UserId) -> MyAppResult<()> {
770                todo!()
771            }
772        };
773
774        let parsed = parse_handler(&method).expect("Should parse successfully");
775        assert_eq!(parsed.fn_name.to_string(), "delete_user");
776        assert_eq!(parsed.method, "DELETE");
777    }
778
779    #[test]
780    fn parse_handler_patch() {
781        let method: ImplItemFn = parse_quote! {
782            #[api_handler(method = "PATCH", path = "/users/{id}")]
783            pub async fn patch_user(&self, #[path] id: UserId, #[body] req: PatchRequest) -> MyAppResult<PatchResponse> {
784                todo!()
785            }
786        };
787
788        let parsed = parse_handler(&method).expect("Should parse successfully");
789        assert_eq!(parsed.method, "PATCH");
790    }
791
792    #[test]
793    fn parse_handler_lowercase_method() {
794        let method: ImplItemFn = parse_quote! {
795            #[api_handler(method = "get", path = "/health")]
796            pub async fn health(&self) -> MyAppResult<String> {
797                todo!()
798            }
799        };
800
801        let parsed = parse_handler(&method).expect("Should parse successfully");
802        assert_eq!(parsed.method, "GET"); // Should be uppercased
803    }
804
805    #[test]
806    fn parse_handler_no_api_handler_attr_returns_none() {
807        let method: ImplItemFn = parse_quote! {
808            pub fn regular_method(&self) -> String {
809                "hello".to_string()
810            }
811        };
812
813        assert!(parse_handler(&method).is_none());
814    }
815
816    #[test]
817    fn parse_handler_no_params() {
818        let method: ImplItemFn = parse_quote! {
819            #[api_handler(method = "GET", path = "/health")]
820            pub async fn health(&self) -> MyAppResult<String> {
821                todo!()
822            }
823        };
824
825        let parsed = parse_handler(&method).expect("Should parse successfully");
826        assert!(parsed.params.is_empty());
827    }
828
829    #[test]
830    fn parse_handler_get_with_query() {
831        let method: ImplItemFn = parse_quote! {
832            #[api_handler(method = "GET", path = "/users")]
833            pub async fn list_users(&self, #[query] params: ListUsersParams) -> MyAppResult<Vec<User>> {
834                todo!()
835            }
836        };
837
838        let parsed = parse_handler(&method).expect("Should parse successfully");
839        assert_eq!(parsed.fn_name.to_string(), "list_users");
840        assert_eq!(parsed.method, "GET");
841        assert_eq!(parsed.path, "/users");
842        assert_eq!(parsed.params.len(), 1);
843        assert_eq!(parsed.params[0].name.to_string(), "params");
844        assert_eq!(parsed.params[0].kind, ParamKind::Query);
845    }
846
847    #[test]
848    fn parse_handler_get_with_path_and_query() {
849        let method: ImplItemFn = parse_quote! {
850            #[api_handler(method = "GET", path = "/users/{id}/posts")]
851            pub async fn list_user_posts(&self, #[path] id: UserId, #[query] params: Pagination) -> MyAppResult<Vec<Post>> {
852                todo!()
853            }
854        };
855
856        let parsed = parse_handler(&method).expect("Should parse successfully");
857        assert_eq!(parsed.fn_name.to_string(), "list_user_posts");
858        assert_eq!(parsed.params.len(), 2);
859        assert_eq!(parsed.params[0].name.to_string(), "id");
860        assert_eq!(parsed.params[0].kind, ParamKind::Path);
861        assert_eq!(parsed.params[1].name.to_string(), "params");
862        assert_eq!(parsed.params[1].kind, ParamKind::Query);
863    }
864
865    #[test]
866    fn parse_handler_preserves_visibility() {
867        let method: ImplItemFn = parse_quote! {
868            #[api_handler(method = "GET", path = "/internal")]
869            pub(crate) async fn internal_endpoint(&self) -> MyAppResult<String> {
870                todo!()
871            }
872        };
873
874        let parsed = parse_handler(&method).expect("Should parse successfully");
875        // Check visibility is preserved (pub(crate))
876        let vis_str = quote!(#(parsed.visibility)).to_string();
877        assert!(
878            vis_str.contains("crate")
879                || matches!(parsed.visibility, syn::Visibility::Restricted(_))
880        );
881    }
882
883    #[test]
884    fn parse_handler_strips_attrs_in_clean_method() {
885        let method: ImplItemFn = parse_quote! {
886            #[api_handler(method = "POST", path = "/users")]
887            pub async fn create_user(&self, #[body] req: CreateUserRequest) -> MyAppResult<CreateUserResponse> {
888                todo!()
889            }
890        };
891
892        let parsed = parse_handler(&method).expect("Should parse successfully");
893
894        // Check that api_handler attr is stripped from clean_method
895        let has_api_handler = parsed
896            .clean_method
897            .attrs
898            .iter()
899            .any(|a| a.path().is_ident("api_handler"));
900        assert!(!has_api_handler, "api_handler should be stripped");
901
902        // Check that #[body] is stripped from params in clean_method
903        for arg in parsed.clean_method.sig.inputs.iter().skip(1) {
904            if let FnArg::Typed(pat_type) = arg {
905                let has_body = pat_type.attrs.iter().any(|a| a.path().is_ident("body"));
906                assert!(!has_body, "#[body] should be stripped from clean_method");
907            }
908        }
909    }
910
911    // ── Tests for generate_axum_handler() ───────────────────────────────────
912
913    #[test]
914    fn generate_axum_handler_includes_state() {
915        let method: ImplItemFn = parse_quote! {
916            #[api_handler(method = "GET", path = "/health")]
917            pub async fn health(&self) -> MyAppResult<String> {
918                todo!()
919            }
920        };
921
922        let parsed = parse_handler(&method).unwrap();
923        let struct_name: syn::Ident = parse_quote!(MyApp);
924        let generated = generate_axum_handler(&struct_name, &parsed);
925        let code = generated.to_string();
926
927        assert!(code.contains("State"));
928        assert!(code.contains("MyApp"));
929        assert!(code.contains("__axum_handler_health"));
930    }
931
932    #[test]
933    fn generate_axum_handler_with_body() {
934        let method: ImplItemFn = parse_quote! {
935            #[api_handler(method = "POST", path = "/users")]
936            pub async fn create_user(&self, #[body] req: CreateUserRequest) -> MyAppResult<CreateUserResponse> {
937                todo!()
938            }
939        };
940
941        let parsed = parse_handler(&method).unwrap();
942        let struct_name: syn::Ident = parse_quote!(MyApp);
943        let generated = generate_axum_handler(&struct_name, &parsed);
944        let code = generated.to_string();
945
946        assert!(code.contains("Json"));
947        assert!(code.contains("CreateUserRequest"));
948    }
949
950    #[test]
951    fn generate_axum_handler_with_path_param() {
952        let method: ImplItemFn = parse_quote! {
953            #[api_handler(method = "GET", path = "/users/{id}")]
954            pub async fn get_user(&self, #[path] id: UserId) -> MyAppResult<GetUserResponse> {
955                todo!()
956            }
957        };
958
959        let parsed = parse_handler(&method).unwrap();
960        let struct_name: syn::Ident = parse_quote!(MyApp);
961        let generated = generate_axum_handler(&struct_name, &parsed);
962        let code = generated.to_string();
963
964        assert!(code.contains("Path"));
965        assert!(code.contains("UserId"));
966    }
967
968    #[test]
969    fn generate_axum_handler_with_query_param() {
970        let method: ImplItemFn = parse_quote! {
971            #[api_handler(method = "GET", path = "/users")]
972            pub async fn list_users(&self, #[query] params: ListUsersParams) -> MyAppResult<Vec<User>> {
973                todo!()
974            }
975        };
976
977        let parsed = parse_handler(&method).unwrap();
978        let struct_name: syn::Ident = parse_quote!(MyApp);
979        let generated = generate_axum_handler(&struct_name, &parsed);
980        let code = generated.to_string();
981
982        assert!(code.contains("Query"));
983        assert!(code.contains("ListUsersParams"));
984    }
985
986    #[test]
987    fn generate_axum_handler_with_path_and_query() {
988        let method: ImplItemFn = parse_quote! {
989            #[api_handler(method = "GET", path = "/users/{id}/posts")]
990            pub async fn list_user_posts(&self, #[path] id: UserId, #[query] params: Pagination) -> MyAppResult<Vec<Post>> {
991                todo!()
992            }
993        };
994
995        let parsed = parse_handler(&method).unwrap();
996        let struct_name: syn::Ident = parse_quote!(MyApp);
997        let generated = generate_axum_handler(&struct_name, &parsed);
998        let code = generated.to_string();
999
1000        assert!(code.contains("Path"));
1001        assert!(code.contains("UserId"));
1002        assert!(code.contains("Query"));
1003        assert!(code.contains("Pagination"));
1004    }
1005
1006    // ── Tests for generate_router() ─────────────────────────────────────────
1007
1008    #[test]
1009    fn generate_router_creates_routes() {
1010        let method1: ImplItemFn = parse_quote! {
1011            #[api_handler(method = "GET", path = "/users")]
1012            pub async fn list_users(&self) -> MyAppResult<Vec<User>> {
1013                todo!()
1014            }
1015        };
1016        let method2: ImplItemFn = parse_quote! {
1017            #[api_handler(method = "POST", path = "/users")]
1018            pub async fn create_user(&self, #[body] req: CreateUserRequest) -> MyAppResult<User> {
1019                todo!()
1020            }
1021        };
1022
1023        let handlers = vec![
1024            parse_handler(&method1).unwrap(),
1025            parse_handler(&method2).unwrap(),
1026        ];
1027        let struct_name: syn::Ident = parse_quote!(MyApp);
1028        let generated = generate_router(&struct_name, &handlers);
1029        let code = generated.to_string();
1030
1031        assert!(code.contains("Router"));
1032        assert!(code.contains("route"));
1033        assert!(code.contains("\"/users\""));
1034        assert!(code.contains("get"));
1035        assert!(code.contains("post"));
1036    }
1037
1038    // ── Tests for generate_client() ─────────────────────────────────────────
1039
1040    #[test]
1041    fn generate_client_creates_struct() {
1042        let method: ImplItemFn = parse_quote! {
1043            #[api_handler(method = "GET", path = "/health")]
1044            pub async fn health(&self) -> MyAppResult<String> {
1045                todo!()
1046            }
1047        };
1048
1049        let handlers = vec![parse_handler(&method).unwrap()];
1050        let struct_name: syn::Ident = parse_quote!(MyApp);
1051        let generated = generate_client(&struct_name, &handlers);
1052        let code = generated.to_string();
1053
1054        assert!(code.contains("MyAppClient"));
1055        assert!(code.contains("MyAppClientError"));
1056        assert!(code.contains("base_url"));
1057        assert!(code.contains("reqwest :: Client"));
1058    }
1059
1060    #[test]
1061    fn generate_client_creates_error_type() {
1062        let method: ImplItemFn = parse_quote! {
1063            #[api_handler(method = "GET", path = "/health")]
1064            pub async fn health(&self) -> MyAppResult<String> {
1065                todo!()
1066            }
1067        };
1068
1069        let handlers = vec![parse_handler(&method).unwrap()];
1070        let struct_name: syn::Ident = parse_quote!(MyApp);
1071        let generated = generate_client(&struct_name, &handlers);
1072        let code = generated.to_string();
1073
1074        assert!(code.contains("Request"));
1075        assert!(code.contains("Api"));
1076        assert!(code.contains("status"));
1077        assert!(code.contains("body"));
1078    }
1079
1080    #[test]
1081    fn generate_client_method_with_body_takes_reference() {
1082        let method: ImplItemFn = parse_quote! {
1083            #[api_handler(method = "POST", path = "/users")]
1084            pub async fn create_user(&self, #[body] req: CreateUserRequest) -> MyAppResult<User> {
1085                todo!()
1086            }
1087        };
1088
1089        let handlers = vec![parse_handler(&method).unwrap()];
1090        let struct_name: syn::Ident = parse_quote!(MyApp);
1091        let generated = generate_client(&struct_name, &handlers);
1092        let code = generated.to_string();
1093
1094        // Body param should be taken by reference
1095        assert!(code.contains("req : & CreateUserRequest"));
1096    }
1097
1098    #[test]
1099    fn generate_client_method_with_path_takes_value() {
1100        let method: ImplItemFn = parse_quote! {
1101            #[api_handler(method = "GET", path = "/users/{id}")]
1102            pub async fn get_user(&self, #[path] id: UserId) -> MyAppResult<User> {
1103                todo!()
1104            }
1105        };
1106
1107        let handlers = vec![parse_handler(&method).unwrap()];
1108        let struct_name: syn::Ident = parse_quote!(MyApp);
1109        let generated = generate_client(&struct_name, &handlers);
1110        let code = generated.to_string();
1111
1112        // Path param should be taken by value (no &)
1113        assert!(code.contains("id : UserId"));
1114        assert!(!code.contains("id : & UserId"));
1115    }
1116
1117    #[test]
1118    fn generate_client_method_with_query_takes_reference() {
1119        let method: ImplItemFn = parse_quote! {
1120            #[api_handler(method = "GET", path = "/users")]
1121            pub async fn list_users(&self, #[query] params: ListUsersParams) -> MyAppResult<Vec<User>> {
1122                todo!()
1123            }
1124        };
1125
1126        let handlers = vec![parse_handler(&method).unwrap()];
1127        let struct_name: syn::Ident = parse_quote!(MyApp);
1128        let generated = generate_client(&struct_name, &handlers);
1129        let code = generated.to_string();
1130
1131        // Query param should be taken by reference
1132        assert!(code.contains("params : & ListUsersParams"));
1133        // Should use serde_urlencoded
1134        assert!(code.contains("serde_urlencoded"));
1135    }
1136
1137    #[test]
1138    fn generate_client_method_with_path_and_query() {
1139        let method: ImplItemFn = parse_quote! {
1140            #[api_handler(method = "GET", path = "/users/{id}/posts")]
1141            pub async fn list_user_posts(&self, #[path] id: UserId, #[query] params: Pagination) -> MyAppResult<Vec<Post>> {
1142                todo!()
1143            }
1144        };
1145
1146        let handlers = vec![parse_handler(&method).unwrap()];
1147        let struct_name: syn::Ident = parse_quote!(MyApp);
1148        let generated = generate_client(&struct_name, &handlers);
1149        let code = generated.to_string();
1150
1151        // Path param should be taken by value
1152        assert!(code.contains("id : UserId"));
1153        // Query param should be taken by reference
1154        assert!(code.contains("params : & Pagination"));
1155        // Should use serde_urlencoded
1156        assert!(code.contains("serde_urlencoded"));
1157    }
1158}