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#[derive(Debug, FromMeta)]
12struct ApiHandlerArgs {
13 method: String,
14 path: String,
15}
16
17#[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
32struct ParsedHandler {
34 fn_name: syn::Ident,
35 method: String,
36 path: String,
37 params: Vec<HandlerParam>,
38 return_type: Box<Type>,
39 clean_method: ImplItemFn,
41 visibility: syn::Visibility,
42}
43
44fn 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
60fn 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
82fn 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
96fn path_to_axum(path: &str) -> &str {
99 path
100}
101
102fn parse_handler(method: &ImplItemFn) -> Option<ParsedHandler> {
103 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 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 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 let mut clean_method = method.clone();
143 clean_method.attrs = strip_api_handler_attr(&clean_method.attrs);
144
145 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
163fn 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 let mut extractor_params: Vec<TokenStream2> = Vec::new();
170 let mut call_args: Vec<TokenStream2> = Vec::new();
171
172 extractor_params.push(quote! {
174 axum::extract::State(state): axum::extract::State<std::sync::Arc<#struct_name>>
175 });
176
177 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 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 for param in handler.params.iter().filter(|p| p.kind == ParamKind::Query) {
205 let name = ¶m.name;
206 let ty = ¶m.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 for param in handler.params.iter().filter(|p| p.kind == ParamKind::Body) {
215 let name = ¶m.name;
216 let ty = ¶m.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
232fn 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 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
264fn 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 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 = ¶m.name;
284 let ty = ¶m.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 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 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 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 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 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 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#[proc_macro_attribute]
430pub fn api(_attr: TokenStream, item: TokenStream) -> TokenStream {
431 let mut input = parse_macro_input!(item as ItemImpl);
432
433 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 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 cleaned_items.push(item.clone());
458 }
459 } else {
460 cleaned_items.push(item.clone());
461 }
462 }
463
464 input.items = cleaned_items;
465
466 let axum_handlers: Vec<TokenStream2> = handlers
468 .iter()
469 .map(|h| generate_axum_handler(&struct_name, h))
470 .collect();
471
472 let router_impl = generate_router(&struct_name, &handlers);
474
475 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 #(#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 #[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 #[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 #[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 #[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 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 #[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 #[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"); }
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 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 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 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 #[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 #[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 #[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 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 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 assert!(code.contains("params : & ListUsersParams"));
1133 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 assert!(code.contains("id : UserId"));
1153 assert!(code.contains("params : & Pagination"));
1155 assert!(code.contains("serde_urlencoded"));
1157 }
1158}