1use crate::extractor::{
2 HttpMethod, Parameter, ParameterLocation, RouteExtractor, RouteInfo, TypeInfo,
3};
4use crate::parser::ParsedFile;
5use syn::{visit::Visit, Expr, ExprCall, ExprMethodCall, Lit};
6
7use log::{debug, warn};
8
9pub struct AxumExtractor;
11
12impl RouteExtractor for AxumExtractor {
13 fn extract_routes(&self, parsed_files: &[ParsedFile]) -> Vec<RouteInfo> {
14 let mut visitor = AxumVisitor::new();
15
16 for parsed_file in parsed_files {
18 visitor.visit_file(&parsed_file.syntax_tree);
19 }
20
21 visitor.analyze_handlers();
23
24 visitor.routes
25 }
26}
27
28struct AxumVisitor {
30 routes: Vec<RouteInfo>,
31 current_prefix: String,
32 functions: std::collections::HashMap<String, syn::Signature>,
33}
34
35impl AxumVisitor {
36 fn new() -> Self {
37 Self {
38 routes: Vec::new(),
39 current_prefix: String::new(),
40 functions: std::collections::HashMap::new(),
41 }
42 }
43
44 fn analyze_handlers(&mut self) {
46 debug!(
47 "Analyzing handlers. Found {} functions and {} routes",
48 self.functions.len(),
49 self.routes.len()
50 );
51
52 let routes_to_update: Vec<_> = self
54 .routes
55 .iter()
56 .enumerate()
57 .map(|(idx, route)| (idx, route.handler_name.clone()))
58 .collect();
59
60 for (idx, handler_name) in routes_to_update {
61 if let Some(fn_sig) = self.functions.get(&handler_name) {
62 debug!("Found handler function: {}", handler_name);
63 let (params, request_body) = self.parse_extractors(fn_sig);
64 let response_type = self.parse_response_type(fn_sig);
65
66 let mut all_params = self.routes[idx].parameters.clone();
68 all_params.extend(params);
69
70 self.routes[idx].parameters = all_params;
71 self.routes[idx].request_body = request_body;
72 self.routes[idx].response_type = response_type;
73 } else {
74 warn!(
80 "Unknown handler: {}",
81 handler_name,
82 );
83 }
84 }
85 }
86
87 fn parse_single_method(&mut self, expr: &ExprMethodCall, prefix: &str) {
89 let method_name = expr.method.to_string();
90
91 match method_name.as_str() {
92 "route" => {
93 if let Some(route_info) = self.parse_route_method(expr, prefix) {
94 self.routes.push(route_info);
95 }
96 }
97 "get" | "post" | "put" | "delete" | "patch" | "head" | "options" => {
98 if let Some(route_info) = self.parse_shorthand_method(expr, prefix, &method_name) {
99 self.routes.push(route_info);
100 }
101 }
102 "nest" => {
103 if let Some(nested_prefix) = self.parse_nest_method(expr, prefix) {
104 if let Some(nested_expr) = expr.args.iter().nth(1) {
106 self.parse_router_expr(nested_expr, nested_prefix);
107 }
108 }
109 }
110 _ => {}
111 }
112 }
113
114 fn parse_route_method(&self, expr: &ExprMethodCall, prefix: &str) -> Option<RouteInfo> {
116 if expr.args.len() < 2 {
118 return None;
119 }
120
121 let path = self.extract_string_literal(&expr.args[0])?;
122 let full_path = self.combine_paths(prefix, &path);
123
124 if let Expr::Call(call_expr) = &expr.args[1] {
127 if let Expr::Path(path_expr) = &*call_expr.func {
128 if let Some(segment) = path_expr.path.segments.last() {
129 let method_name = segment.ident.to_string();
130 if let Some(method) = self.parse_http_method(&method_name) {
131 let handler_name = self.extract_handler_name(call_expr);
132 let mut route = RouteInfo::new(full_path.clone(), method, handler_name);
133 route.parameters = self.extract_path_parameters(&full_path);
134 return Some(route);
135 }
136 }
137 }
138 }
139
140 None
141 }
142
143 fn parse_shorthand_method(
145 &self,
146 expr: &ExprMethodCall,
147 prefix: &str,
148 method_name: &str,
149 ) -> Option<RouteInfo> {
150 if expr.args.is_empty() {
152 return None;
153 }
154
155 let method = self.parse_http_method(method_name)?;
156
157 if let Some(path) = self.extract_string_literal(&expr.args[0]) {
159 let full_path = self.combine_paths(prefix, &path);
161 let handler_name = if expr.args.len() > 1 {
162 self.extract_handler_name_from_expr(&expr.args[1])
163 } else {
164 return None;
166 };
167 let mut route = RouteInfo::new(full_path.clone(), method, handler_name);
168 route.parameters = self.extract_path_parameters(&full_path);
169 Some(route)
170 } else {
171 let handler_name = self.extract_handler_name_from_expr(&expr.args[0]);
173 if prefix.len() < 1 {
174 warn!("Ignored handler with 0-length path: {}", handler_name);
175 None
176 } else {
177 let mut route = RouteInfo::new(prefix.to_string(), method, handler_name);
178 route.parameters = self.extract_path_parameters(prefix);
179 Some(route)
180 }
181 }
182 }
183
184 fn parse_nest_method(&self, expr: &ExprMethodCall, prefix: &str) -> Option<String> {
186 if expr.args.is_empty() {
188 return None;
189 }
190
191 let path = self.extract_string_literal(&expr.args[0])?;
192 Some(self.combine_paths(prefix, &path))
193 }
194
195 fn parse_router_expr(&mut self, _expr: &Expr, _prefix: String) {
197 }
200
201 fn extract_string_literal(&self, expr: &Expr) -> Option<String> {
203 match expr {
204 Expr::Lit(expr_lit) => {
205 if let Lit::Str(lit_str) = &expr_lit.lit {
206 Some(lit_str.value())
207 } else {
208 None
209 }
210 }
211 _ => None,
212 }
213 }
214
215 fn combine_paths(&self, prefix: &str, path: &str) -> String {
217 if prefix.is_empty() {
218 return path.to_string();
219 }
220
221 let prefix = prefix.trim_end_matches('/');
222 let path = path.trim_start_matches('/');
223
224 if path.is_empty() {
225 prefix.to_string()
226 } else {
227 format!("{}/{}", prefix, path)
228 }
229 }
230
231 fn parse_http_method(&self, method: &str) -> Option<HttpMethod> {
233 match method.to_lowercase().as_str() {
234 "get" => Some(HttpMethod::Get),
235 "post" => Some(HttpMethod::Post),
236 "put" => Some(HttpMethod::Put),
237 "delete" => Some(HttpMethod::Delete),
238 "patch" => Some(HttpMethod::Patch),
239 "head" => Some(HttpMethod::Head),
240 "options" => Some(HttpMethod::Options),
241 _ => None,
242 }
243 }
244
245 fn extract_handler_name(&self, call_expr: &ExprCall) -> String {
247 if let Some(arg) = call_expr.args.first() {
248 self.extract_handler_name_from_expr(arg)
249 } else {
250 "unknown".to_string()
251 }
252 }
253
254 fn extract_handler_name_from_expr(&self, expr: &Expr) -> String {
256 match expr {
257 Expr::Path(path_expr) => path_expr
258 .path
259 .segments
260 .last()
261 .map(|s| s.ident.to_string())
262 .unwrap_or_else(|| "unknown".to_string()),
263 _ => "unknown".to_string(),
264 }
265 }
266
267 fn extract_path_parameters(&self, path: &str) -> Vec<Parameter> {
269 let mut parameters = Vec::new();
270
271 for segment in path.split('/') {
272 if segment.starts_with(':') {
273 let param_name = segment.trim_start_matches(':').to_string();
274 parameters.push(Parameter::new(
275 param_name,
276 ParameterLocation::Path,
277 TypeInfo::new("String".to_string()),
278 true,
279 ));
280 }
281 }
282
283 parameters
284 }
285
286 fn parse_response_type(&self, fn_sig: &syn::Signature) -> Option<TypeInfo> {
288 match &fn_sig.output {
290 syn::ReturnType::Default => None,
291 syn::ReturnType::Type(_, ty) => {
292 self.parse_return_type(ty)
294 }
295 }
296 }
297
298 fn parse_return_type(&self, ty: &syn::Type) -> Option<TypeInfo> {
300 match ty {
301 syn::Type::ImplTrait(_) => {
303 None
305 }
306 syn::Type::Reference(type_ref) => {
308 Some(self.extract_type_info(&type_ref.elem))
310 }
311 syn::Type::Path(type_path) => {
313 if let Some(segment) = type_path.path.segments.last() {
314 let type_name = segment.ident.to_string();
315
316 if type_name == "Json" {
318 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
319 if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
320 return Some(self.extract_type_info(inner_ty));
321 }
322 }
323 }
324
325 if type_name == "Result" {
327 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
328 if let Some(syn::GenericArgument::Type(ok_ty)) = args.args.first() {
329 return self.parse_return_type(ok_ty);
331 }
332 }
333 }
334
335 Some(self.extract_type_info(ty))
341 } else {
342 None
343 }
344 }
345 syn::Type::Tuple(tuple) => {
347 for elem in &tuple.elems {
349 if let Some(type_info) = self.extract_json_from_type(elem) {
350 return Some(type_info);
351 }
352 }
353 None
354 }
355 _ => None,
356 }
357 }
358
359 fn extract_json_from_type(&self, ty: &syn::Type) -> Option<TypeInfo> {
361 if let syn::Type::Path(type_path) = ty {
362 if let Some(segment) = type_path.path.segments.last() {
363 if segment.ident == "Json" {
364 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
365 if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
366 return Some(self.extract_type_info(inner_ty));
367 }
368 }
369 }
370 }
371 }
372 None
373 }
374
375 fn parse_extractors(&self, fn_sig: &syn::Signature) -> (Vec<Parameter>, Option<TypeInfo>) {
377 let mut parameters = Vec::new();
378 let mut request_body = None;
379
380 for input in &fn_sig.inputs {
381 if let syn::FnArg::Typed(pat_type) = input {
382 if let Some((extractor_type, inner_type)) = self.parse_extractor_type(&pat_type.ty)
384 {
385 match extractor_type.as_str() {
386 "Json" => {
387 request_body = Some(inner_type);
389 }
390 "Path" => {
391 parameters.push(Parameter::new(
395 "path_params".to_string(),
396 ParameterLocation::Path,
397 inner_type,
398 true,
399 ));
400 }
401 "Query" => {
402 parameters.push(Parameter::new(
404 "query_params".to_string(),
405 ParameterLocation::Query,
406 inner_type,
407 false,
408 ));
409 }
410 _ => {}
411 }
412 }
413 }
414 }
415
416 (parameters, request_body)
417 }
418
419 fn parse_extractor_type(&self, ty: &syn::Type) -> Option<(String, TypeInfo)> {
421 if let syn::Type::Path(type_path) = ty {
422 if let Some(segment) = type_path.path.segments.last() {
423 let extractor_name = segment.ident.to_string();
424
425 if matches!(extractor_name.as_str(), "Json" | "Path" | "Query") {
427 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
429 if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
430 let type_info = self.extract_type_info(inner_ty);
431 return Some((extractor_name, type_info));
432 }
433 }
434 }
435 }
436 }
437 None
438 }
439
440 fn extract_type_info(&self, ty: &syn::Type) -> TypeInfo {
442 match ty {
443 syn::Type::Path(type_path) => {
444 if let Some(segment) = type_path.path.segments.last() {
445 let type_name = segment.ident.to_string();
446
447 if type_name == "Option" {
449 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
450 if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
451 let inner_type_info = self.extract_type_info(inner_ty);
452 return TypeInfo::option(inner_type_info);
453 }
454 }
455 }
456
457 if type_name == "Vec" {
459 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
460 if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
461 let inner_type_info = self.extract_type_info(inner_ty);
462 return TypeInfo::vec(inner_type_info);
463 }
464 }
465 }
466
467 TypeInfo::new(type_name)
469 } else {
470 TypeInfo::new("unknown".to_string())
471 }
472 }
473 _ => TypeInfo::new("unknown".to_string()),
474 }
475 }
476}
477
478impl<'ast> Visit<'ast> for AxumVisitor {
479 fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
480 let method_name = node.method.to_string();
481
482 if matches!(
485 method_name.as_str(),
486 "route" | "get" | "post" | "put" | "delete" | "patch" | "head" | "options" | "nest"
487 ) {
488 self.parse_single_method(node, &self.current_prefix.clone());
490 }
491
492 syn::visit::visit_expr_method_call(self, node);
494 }
495
496 fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
497 let fn_name = node.sig.ident.to_string();
499 debug!("Found function: {}", fn_name);
500 self.functions.insert(fn_name, node.sig.clone());
501
502 syn::visit::visit_item_fn(self, node);
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use std::path::PathBuf;
511
512 fn parse_code(code: &str) -> ParsedFile {
513 let syntax_tree = syn::parse_file(code).expect("Failed to parse test code");
514 ParsedFile {
515 path: PathBuf::from("test.rs"),
516 syntax_tree,
517 }
518 }
519
520 #[test]
521 fn test_simple_route_extraction() {
522 let code = r#"
523 use axum::{Router, routing::get};
524
525 async fn handler() -> &'static str {
526 "Hello, World!"
527 }
528
529 fn app() -> Router {
530 Router::new().route("/hello", get(handler))
531 }
532 "#;
533
534 let parsed = parse_code(code);
535 let extractor = AxumExtractor;
536 let routes = extractor.extract_routes(&[parsed]);
537
538 assert_eq!(routes.len(), 1);
539 assert_eq!(routes[0].path, "/hello");
540 assert_eq!(routes[0].method, HttpMethod::Get);
541 assert_eq!(routes[0].handler_name, "handler");
542 }
543
544 #[test]
545 fn test_shorthand_methods() {
546 let code = r#"
547 use axum::{Router, routing::{get, post}};
548
549 async fn get_handler() {}
550 async fn post_handler() {}
551
552 fn app() -> Router {
553 Router::new()
554 .route("/users", get(get_handler))
555 .route("/users", post(post_handler))
556 }
557 "#;
558
559 let parsed = parse_code(code);
560 let extractor = AxumExtractor;
561 let routes = extractor.extract_routes(&[parsed]);
562
563 assert!(
566 routes.len() >= 2,
567 "Expected at least 2 routes, got {}",
568 routes.len()
569 );
570
571 let get_route = routes.iter().find(|r| r.method == HttpMethod::Get).unwrap();
572 assert_eq!(get_route.path, "/users");
573 assert_eq!(get_route.handler_name, "get_handler");
574
575 let post_route = routes
576 .iter()
577 .find(|r| r.method == HttpMethod::Post)
578 .unwrap();
579 assert_eq!(post_route.path, "/users");
580 assert_eq!(post_route.handler_name, "post_handler");
581 }
582
583 #[test]
584 fn test_path_parameters() {
585 let code = r#"
586 use axum::{Router, routing::get};
587
588 async fn get_user() {}
589
590 fn app() -> Router {
591 Router::new().route("/users/:id", get(get_user))
592 }
593 "#;
594
595 let parsed = parse_code(code);
596 let extractor = AxumExtractor;
597 let routes = extractor.extract_routes(&[parsed]);
598
599 assert_eq!(routes.len(), 1);
600 assert_eq!(routes[0].path, "/users/:id");
601 assert_eq!(routes[0].parameters.len(), 1);
602 assert_eq!(routes[0].parameters[0].name, "id");
603 assert_eq!(routes[0].parameters[0].location, ParameterLocation::Path);
604 assert!(routes[0].parameters[0].required);
605 }
606
607 #[test]
608 fn test_nested_routes() {
609 let code = r#"
610 use axum::{Router, routing::get};
611
612 async fn list_users() {}
613 async fn get_user() {}
614
615 fn users_router() -> Router {
616 Router::new()
617 .route("/", get(list_users))
618 .route("/:id", get(get_user))
619 }
620
621 fn app() -> Router {
622 Router::new().nest("/api/users", users_router())
623 }
624 "#;
625
626 let parsed = parse_code(code);
627 let extractor = AxumExtractor;
628 let routes = extractor.extract_routes(&[parsed]);
629
630 assert!(!routes.is_empty());
636 }
637
638 #[test]
639 fn test_multiple_path_parameters() {
640 let code = r#"
641 use axum::{Router, routing::get};
642
643 async fn get_comment() {}
644
645 fn app() -> Router {
646 Router::new().route("/posts/:post_id/comments/:comment_id", get(get_comment))
647 }
648 "#;
649
650 let parsed = parse_code(code);
651 let extractor = AxumExtractor;
652 let routes = extractor.extract_routes(&[parsed]);
653
654 assert_eq!(routes.len(), 1);
655 assert_eq!(routes[0].path, "/posts/:post_id/comments/:comment_id");
656 assert_eq!(routes[0].parameters.len(), 2);
657
658 let param_names: Vec<_> = routes[0]
659 .parameters
660 .iter()
661 .map(|p| p.name.as_str())
662 .collect();
663 assert!(param_names.contains(&"post_id"));
664 assert!(param_names.contains(&"comment_id"));
665 }
666
667 #[test]
668 fn test_extractor_recognition() {
669 let code = r#"
670 use axum::{Router, routing::post, Json, extract::Path};
671 use serde::Deserialize;
672
673 #[derive(Deserialize)]
674 struct CreateUser {
675 name: String,
676 }
677
678 async fn create_user(
679 Path(id): Path<u32>,
680 Json(payload): Json<CreateUser>,
681 ) -> String {
682 format!("Created user {} with id {}", payload.name, id)
683 }
684
685 fn app() -> Router {
686 Router::new().route("/users/:id", post(create_user))
687 }
688 "#;
689
690 let parsed = parse_code(code);
691 let extractor = AxumExtractor;
692 let routes = extractor.extract_routes(&[parsed]);
693
694 assert_eq!(routes.len(), 1);
695 assert_eq!(routes[0].handler_name, "create_user");
696
697 let path_params: Vec<_> = routes[0]
700 .parameters
701 .iter()
702 .filter(|p| p.location == ParameterLocation::Path)
703 .collect();
704 assert!(!path_params.is_empty());
705
706 assert!(routes[0].request_body.is_some());
708 if let Some(ref body) = routes[0].request_body {
709 assert_eq!(body.name, "CreateUser");
710 }
711 }
712
713 #[test]
714 fn test_query_parameters() {
715 let code = r#"
716 use axum::{Router, routing::get, extract::Query};
717 use serde::Deserialize;
718
719 #[derive(Deserialize)]
720 struct Pagination {
721 page: u32,
722 limit: u32,
723 }
724
725 async fn list_users(Query(params): Query<Pagination>) -> String {
726 format!("Page {} with limit {}", params.page, params.limit)
727 }
728
729 fn app() -> Router {
730 Router::new().route("/users", get(list_users))
731 }
732 "#;
733
734 let parsed = parse_code(code);
735 let extractor = AxumExtractor;
736 let routes = extractor.extract_routes(&[parsed]);
737
738 assert_eq!(routes.len(), 1);
739
740 let query_params: Vec<_> = routes[0]
742 .parameters
743 .iter()
744 .filter(|p| p.location == ParameterLocation::Query)
745 .collect();
746 assert!(!query_params.is_empty());
747
748 if let Some(param) = query_params.first() {
749 assert_eq!(param.type_info.name, "Pagination");
750 }
751 }
752
753 #[test]
754 fn test_multiple_http_methods() {
755 let code = r#"
756 use axum::{Router, routing::{get, post, put, delete, patch}};
757
758 async fn get_handler() {}
759 async fn post_handler() {}
760 async fn put_handler() {}
761 async fn delete_handler() {}
762 async fn patch_handler() {}
763
764 fn app() -> Router {
765 Router::new()
766 .route("/resource", get(get_handler))
767 .route("/resource", post(post_handler))
768 .route("/resource", put(put_handler))
769 .route("/resource", delete(delete_handler))
770 .route("/resource", patch(patch_handler))
771 }
772 "#;
773
774 let parsed = parse_code(code);
775 let extractor = AxumExtractor;
776 let routes = extractor.extract_routes(&[parsed]);
777
778 assert_eq!(routes.len(), 5);
779
780 let methods: Vec<_> = routes.iter().map(|r| &r.method).collect();
781 assert!(methods.contains(&&HttpMethod::Get));
782 assert!(methods.contains(&&HttpMethod::Post));
783 assert!(methods.contains(&&HttpMethod::Put));
784 assert!(methods.contains(&&HttpMethod::Delete));
785 assert!(methods.contains(&&HttpMethod::Patch));
786 }
787
788 #[test]
789 fn test_json_response_type() {
790 let code = r#"
791 use axum::{Router, routing::get, Json};
792 use serde::Serialize;
793
794 #[derive(Serialize)]
795 struct User {
796 id: u32,
797 name: String,
798 }
799
800 async fn get_user() -> Json<User> {
801 Json(User { id: 1, name: "Test".to_string() })
802 }
803
804 fn app() -> Router {
805 Router::new().route("/user", get(get_user))
806 }
807 "#;
808
809 let parsed = parse_code(code);
810 let extractor = AxumExtractor;
811 let routes = extractor.extract_routes(&[parsed]);
812
813 assert_eq!(routes.len(), 1);
814 assert!(routes[0].response_type.is_some());
815
816 if let Some(ref response) = routes[0].response_type {
817 assert_eq!(response.name, "User");
818 }
819 }
820
821 #[test]
822 fn test_result_json_response_type() {
823 let code = r#"
824 use axum::{Router, routing::get, Json};
825 use serde::Serialize;
826
827 #[derive(Serialize)]
828 struct User {
829 id: u32,
830 name: String,
831 }
832
833 async fn get_user() -> Result<Json<User>, String> {
834 Ok(Json(User { id: 1, name: "Test".to_string() }))
835 }
836
837 fn app() -> Router {
838 Router::new().route("/user", get(get_user))
839 }
840 "#;
841
842 let parsed = parse_code(code);
843 let extractor = AxumExtractor;
844 let routes = extractor.extract_routes(&[parsed]);
845
846 assert_eq!(routes.len(), 1);
847 assert!(routes[0].response_type.is_some());
848
849 if let Some(ref response) = routes[0].response_type {
850 assert_eq!(response.name, "User");
851 }
852 }
853
854 #[test]
855 fn test_tuple_response_with_json() {
856 let code = r#"
857 use axum::{Router, routing::post, Json, http::StatusCode};
858 use serde::Serialize;
859
860 #[derive(Serialize)]
861 struct CreatedUser {
862 id: u32,
863 name: String,
864 }
865
866 async fn create_user() -> (StatusCode, Json<CreatedUser>) {
867 (StatusCode::CREATED, Json(CreatedUser { id: 1, name: "Test".to_string() }))
868 }
869
870 fn app() -> Router {
871 Router::new().route("/user", post(create_user))
872 }
873 "#;
874
875 let parsed = parse_code(code);
876 let extractor = AxumExtractor;
877 let routes = extractor.extract_routes(&[parsed]);
878
879 assert_eq!(routes.len(), 1);
880 assert!(routes[0].response_type.is_some());
881
882 if let Some(ref response) = routes[0].response_type {
883 assert_eq!(response.name, "CreatedUser");
884 }
885 }
886
887 #[test]
888 fn test_vec_response_type() {
889 let code = r#"
890 use axum::{Router, routing::get, Json};
891 use serde::Serialize;
892
893 #[derive(Serialize)]
894 struct User {
895 id: u32,
896 name: String,
897 }
898
899 async fn list_users() -> Json<Vec<User>> {
900 Json(vec![])
901 }
902
903 fn app() -> Router {
904 Router::new().route("/users", get(list_users))
905 }
906 "#;
907
908 let parsed = parse_code(code);
909 let extractor = AxumExtractor;
910 let routes = extractor.extract_routes(&[parsed]);
911
912 assert_eq!(routes.len(), 1);
913 assert!(routes[0].response_type.is_some());
914
915 if let Some(ref response) = routes[0].response_type {
916 assert!(response.is_vec);
917 assert_eq!(response.name, "User");
918 }
919 }
920
921 #[test]
922 fn test_string_response_type() {
923 let code = r#"
924 use axum::{Router, routing::get};
925
926 async fn health_check() -> &'static str {
927 "OK"
928 }
929
930 fn app() -> Router {
931 Router::new().route("/health", get(health_check))
932 }
933 "#;
934
935 let parsed = parse_code(code);
936 let extractor = AxumExtractor;
937 let routes = extractor.extract_routes(&[parsed]);
938
939 assert_eq!(routes.len(), 1);
940 assert!(routes[0].response_type.is_some());
942 }
943
944 #[test]
945 fn test_free_function_detection() {
946 let code = r#"
947 use axum::{Router, routing::get, Json};
948 use serde::Serialize;
949
950 #[derive(Serialize)]
951 struct User {
952 id: u32,
953 name: String,
954 }
955
956 async fn get_user() -> Json<User> {
957 Json(User { id: 1, name: "Test".to_string() })
958 }
959
960 async fn health() -> &'static str {
961 "OK"
962 }
963
964 fn app() -> Router {
965 Router::new()
966 .route("/user", get(get_user))
967 .route("/health", get(health))
968 }
969 "#;
970
971 let parsed = parse_code(code);
972 let extractor = AxumExtractor;
973 let routes = extractor.extract_routes(&[parsed]);
974
975 println!("Found {} routes", routes.len());
976 for route in &routes {
977 println!(
978 " Route: {:?} {} -> {}",
979 route.method, route.path, route.handler_name
980 );
981 if let Some(ref response) = route.response_type {
982 println!(" Response: {}", response.name);
983 }
984 }
985
986 assert_eq!(routes.len(), 2, "Expected 2 routes, found {}", routes.len());
988
989 let user_route = routes.iter().find(|r| r.path == "/user").unwrap();
991 assert_eq!(user_route.handler_name, "get_user");
992 assert!(
993 user_route.response_type.is_some(),
994 "get_user should have response type"
995 );
996 if let Some(ref response) = user_route.response_type {
997 assert_eq!(response.name, "User");
998 }
999
1000 let health_route = routes.iter().find(|r| r.path == "/health").unwrap();
1001 assert_eq!(health_route.handler_name, "health");
1002 assert!(
1003 health_route.response_type.is_some(),
1004 "health should have response type"
1005 );
1006 }
1007}