1use crate::macro_analyzer::HttpMethod;
6use lsp_types::Location;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
13pub struct RouteNavigator {
14 pub index: RouteIndex,
16}
17
18impl RouteNavigator {
19 pub fn new() -> Self {
21 Self {
22 index: RouteIndex::new(),
23 }
24 }
25
26 pub fn build_index(&mut self, documents: &[crate::macro_analyzer::RustDocument]) {
41 self.index.routes.clear();
43 self.index.path_map.clear();
44
45 for doc in documents {
47 for spring_macro in &doc.macros {
49 if let crate::macro_analyzer::SpringMacro::Route(route_macro) = spring_macro {
51 for method in &route_macro.methods {
53 let path_params = self.parse_path_parameters(&route_macro.path);
55
56 let parameters = path_params
60 .iter()
61 .map(|param_name| Parameter {
62 name: param_name.clone(),
63 type_name: "Unknown".to_string(), })
65 .collect();
66
67 let route_info = RouteInfo {
69 path: route_macro.path.clone(),
70 methods: vec![method.clone()],
71 handler: HandlerInfo {
72 function_name: route_macro.handler_name.clone(),
73 parameters,
74 },
75 location: Location {
76 uri: doc.uri.clone(),
77 range: route_macro.range,
78 },
79 };
80
81 let route_index = self.index.routes.len();
83 self.index.routes.push(route_info);
84
85 self.index
87 .path_map
88 .entry(route_macro.path.clone())
89 .or_default()
90 .push(route_index);
91 }
92 }
93 }
94 }
95 }
96
97 pub fn find_routes(&self, pattern: &str) -> Vec<&RouteInfo> {
127 if pattern.is_empty() {
128 return Vec::new();
129 }
130
131 if let Some(regex_pattern) = pattern.strip_prefix("regex:") {
133 if let Ok(re) = regex::Regex::new(regex_pattern) {
135 self.index
136 .routes
137 .iter()
138 .filter(|route| re.is_match(&route.path))
139 .collect()
140 } else {
141 Vec::new()
143 }
144 } else {
145 self.index
147 .routes
148 .iter()
149 .filter(|route| route.path.contains(pattern))
150 .collect()
151 }
152 }
153
154 pub fn get_all_routes(&self) -> &[RouteInfo] {
176 &self.index.routes
177 }
178
179 pub fn find_routes_by_handler(&self, handler_name: &str) -> Vec<&RouteInfo> {
207 self.index
208 .routes
209 .iter()
210 .filter(|route| route.handler.function_name == handler_name)
211 .collect()
212 }
213
214 pub fn validate_routes(&self) -> Vec<lsp_types::Diagnostic> {
245 let mut diagnostics = Vec::new();
246
247 for route in &self.index.routes {
248 diagnostics.extend(self.validate_path_characters(&route.path, &route.location));
250
251 diagnostics.extend(self.validate_path_parameter_syntax(&route.path, &route.location));
253
254 diagnostics.extend(self.validate_path_parameter_types(route));
256
257 diagnostics.extend(self.validate_restful_style(route));
259 }
260
261 diagnostics
262 }
263
264 pub fn detect_conflicts(&self) -> Vec<RouteConflict> {
290 let mut conflicts = Vec::new();
291
292 for i in 0..self.index.routes.len() {
294 for j in (i + 1)..self.index.routes.len() {
295 let route1 = &self.index.routes[i];
296 let route2 = &self.index.routes[j];
297
298 if route1.path == route2.path {
300 for method1 in &route1.methods {
302 if route2.methods.contains(method1) {
303 conflicts.push(RouteConflict {
304 index1: i,
305 index2: j,
306 path: route1.path.clone(),
307 method: method1.clone(),
308 location1: route1.location.clone(),
309 location2: route2.location.clone(),
310 });
311 }
312 }
313 }
314 }
315 }
316
317 conflicts
318 }
319
320 fn validate_path_characters(
328 &self,
329 path: &str,
330 location: &Location,
331 ) -> Vec<lsp_types::Diagnostic> {
332 let mut diagnostics = Vec::new();
333
334 for (i, ch) in path.chars().enumerate() {
337 if !ch.is_ascii_alphanumeric()
338 && !matches!(
339 ch,
340 '-' | '_'
341 | '.'
342 | '~'
343 | ':'
344 | '/'
345 | '?'
346 | '#'
347 | '['
348 | ']'
349 | '@'
350 | '!'
351 | '$'
352 | '&'
353 | '\''
354 | '('
355 | ')'
356 | '*'
357 | '+'
358 | ','
359 | ';'
360 | '='
361 | '{'
362 | '}'
363 )
364 {
365 diagnostics.push(lsp_types::Diagnostic {
366 range: location.range,
367 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
368 code: Some(lsp_types::NumberOrString::String("invalid-path-char".to_string())),
369 message: format!(
370 "路径包含无效字符 '{}' (位置 {})。URL 路径只能包含字母、数字和特定的特殊字符。",
371 ch, i
372 ),
373 source: Some("spring-lsp".to_string()),
374 ..Default::default()
375 });
376 }
377 }
378
379 diagnostics
380 }
381
382 fn validate_path_parameter_syntax(
390 &self,
391 path: &str,
392 location: &Location,
393 ) -> Vec<lsp_types::Diagnostic> {
394 let mut diagnostics = Vec::new();
395 let mut brace_count = 0;
396 let mut last_open_brace = None;
397
398 for (i, ch) in path.chars().enumerate() {
399 match ch {
400 '{' => {
401 if brace_count > 0 {
402 diagnostics.push(lsp_types::Diagnostic {
404 range: location.range,
405 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
406 code: Some(lsp_types::NumberOrString::String(
407 "nested-path-param".to_string(),
408 )),
409 message: format!("路径参数不能嵌套 (位置 {})。正确格式:{{param}}", i),
410 source: Some("spring-lsp".to_string()),
411 ..Default::default()
412 });
413 }
414 brace_count += 1;
415 last_open_brace = Some(i);
416 }
417 '}' => {
418 if brace_count == 0 {
419 diagnostics.push(lsp_types::Diagnostic {
421 range: location.range,
422 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
423 code: Some(lsp_types::NumberOrString::String(
424 "unmatched-closing-brace".to_string(),
425 )),
426 message: format!(
427 "路径参数缺少开括号 '{{' (位置 {})。正确格式:{{param}}",
428 i
429 ),
430 source: Some("spring-lsp".to_string()),
431 ..Default::default()
432 });
433 } else {
434 if let Some(open_pos) = last_open_brace {
436 if i == open_pos + 1 {
437 diagnostics.push(lsp_types::Diagnostic {
438 range: location.range,
439 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
440 code: Some(lsp_types::NumberOrString::String(
441 "empty-path-param".to_string(),
442 )),
443 message: format!(
444 "路径参数名称不能为空 (位置 {})。正确格式:{{param}}",
445 open_pos
446 ),
447 source: Some("spring-lsp".to_string()),
448 ..Default::default()
449 });
450 }
451 }
452 brace_count -= 1;
453 }
454 }
455 _ => {}
456 }
457 }
458
459 if brace_count > 0 {
461 if let Some(open_pos) = last_open_brace {
462 diagnostics.push(lsp_types::Diagnostic {
463 range: location.range,
464 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
465 code: Some(lsp_types::NumberOrString::String(
466 "unclosed-path-param".to_string(),
467 )),
468 message: format!(
469 "路径参数缺少闭括号 '}}' (位置 {})。正确格式:{{param}}",
470 open_pos
471 ),
472 source: Some("spring-lsp".to_string()),
473 ..Default::default()
474 });
475 }
476 }
477
478 diagnostics
479 }
480
481 fn validate_path_parameter_types(&self, route: &RouteInfo) -> Vec<lsp_types::Diagnostic> {
489 let mut diagnostics = Vec::new();
490
491 let path_params = self.parse_path_parameters(&route.path);
493
494 for path_param in &path_params {
496 let found = route
498 .handler
499 .parameters
500 .iter()
501 .any(|p| p.name == *path_param);
502
503 if !found {
504 diagnostics.push(lsp_types::Diagnostic {
505 range: route.location.range,
506 severity: Some(lsp_types::DiagnosticSeverity::WARNING),
507 code: Some(lsp_types::NumberOrString::String(
508 "missing-path-param".to_string(),
509 )),
510 message: format!(
511 "路径参数 '{}' 在处理器函数 '{}' 的参数列表中未找到。\
512 请确保函数参数中包含 Path<T> 类型的参数来接收此路径参数。",
513 path_param, route.handler.function_name
514 ),
515 source: Some("spring-lsp".to_string()),
516 ..Default::default()
517 });
518 }
519 }
520
521 for param in &route.handler.parameters {
523 if path_params.contains(¶m.name) {
524 if !param.type_name.contains("Path")
526 && !param.type_name.contains("Unknown")
527 && !param.type_name.is_empty()
528 {
529 diagnostics.push(lsp_types::Diagnostic {
530 range: route.location.range,
531 severity: Some(lsp_types::DiagnosticSeverity::WARNING),
532 code: Some(lsp_types::NumberOrString::String(
533 "incompatible-path-param-type".to_string(),
534 )),
535 message: format!(
536 "路径参数 '{}' 的类型 '{}' 可能不兼容。\
537 路径参数通常应该使用 Path<T> 类型。",
538 param.name, param.type_name
539 ),
540 source: Some("spring-lsp".to_string()),
541 ..Default::default()
542 });
543 }
544 }
545 }
546
547 diagnostics
548 }
549
550 fn validate_restful_style(&self, route: &RouteInfo) -> Vec<lsp_types::Diagnostic> {
558 let mut diagnostics = Vec::new();
559
560 let segments: Vec<&str> = route.path.split('/').filter(|s| !s.is_empty()).collect();
562
563 for segment in &segments {
564 if segment.starts_with('{') && segment.ends_with('}') {
566 continue;
567 }
568
569 let verbs = [
571 "get", "post", "put", "delete", "patch", "create", "update", "remove", "add",
572 "list", "fetch", "retrieve", "save", "destroy",
573 ];
574
575 let segment_lower = segment.to_lowercase();
576 for verb in &verbs {
577 let is_verb_match = if segment_lower == *verb {
581 true
583 } else if segment_lower.starts_with(verb) && segment_lower.len() > verb.len() {
584 let next_char = segment.chars().nth(verb.len()).unwrap();
586 next_char.is_uppercase() || next_char == '-' || next_char == '_'
588 } else {
589 false
590 };
591
592 if is_verb_match {
593 diagnostics.push(lsp_types::Diagnostic {
594 range: route.location.range,
595 severity: Some(lsp_types::DiagnosticSeverity::INFORMATION),
596 code: Some(lsp_types::NumberOrString::String(
597 "restful-style-verb".to_string(),
598 )),
599 message: format!(
600 "路径段 '{}' 包含动词 '{}'。RESTful API 建议使用名词而非动词,\
601 通过 HTTP 方法(GET、POST、PUT、DELETE)来表示操作。\
602 例如:使用 'GET /users' 而非 'GET /getUsers'。",
603 segment, verb
604 ),
605 source: Some("spring-lsp".to_string()),
606 ..Default::default()
607 });
608 break;
609 }
610 }
611
612 if segment.chars().any(|c| c.is_uppercase()) {
614 diagnostics.push(lsp_types::Diagnostic {
615 range: route.location.range,
616 severity: Some(lsp_types::DiagnosticSeverity::INFORMATION),
617 code: Some(lsp_types::NumberOrString::String(
618 "restful-style-case".to_string(),
619 )),
620 message: format!(
621 "路径段 '{}' 使用了大写字母。RESTful API 建议使用小写字母和连字符。\
622 例如:使用 '/user-profiles' 而非 '/userProfiles'。",
623 segment
624 ),
625 source: Some("spring-lsp".to_string()),
626 ..Default::default()
627 });
628 }
629 }
630
631 diagnostics
632 }
633
634 fn parse_path_parameters(&self, path: &str) -> Vec<String> {
650 let mut parameters = Vec::new();
651 let mut in_param = false;
652 let mut param_start = 0;
653
654 for (i, ch) in path.char_indices() {
655 match ch {
656 '{' => {
657 in_param = true;
658 param_start = i + 1;
659 }
660 '}' => {
661 if in_param {
662 let param_name = &path[param_start..i];
663 if !param_name.is_empty() {
664 parameters.push(param_name.to_string());
665 }
666 in_param = false;
667 }
668 }
669 _ => {}
670 }
671 }
672
673 parameters
674 }
675}
676
677impl Default for RouteNavigator {
678 fn default() -> Self {
679 Self::new()
680 }
681}
682
683#[derive(Debug, Clone)]
687pub struct RouteIndex {
688 pub routes: Vec<RouteInfo>,
690 pub path_map: HashMap<String, Vec<usize>>,
693}
694
695impl RouteIndex {
696 pub fn new() -> Self {
698 Self {
699 routes: Vec::new(),
700 path_map: HashMap::new(),
701 }
702 }
703}
704
705impl Default for RouteIndex {
706 fn default() -> Self {
707 Self::new()
708 }
709}
710
711#[derive(Debug, Clone)]
715pub struct RouteInfo {
716 pub path: String,
718 pub methods: Vec<HttpMethod>,
720 pub handler: HandlerInfo,
722 pub location: Location,
724}
725
726#[derive(Debug, Clone)]
730pub struct HandlerInfo {
731 pub function_name: String,
733 pub parameters: Vec<Parameter>,
735}
736
737#[derive(Debug, Clone)]
741pub struct Parameter {
742 pub name: String,
744 pub type_name: String,
746}
747
748#[derive(Debug, Clone)]
752pub struct RouteConflict {
753 pub index1: usize,
755 pub index2: usize,
757 pub path: String,
759 pub method: HttpMethod,
761 pub location1: Location,
763 pub location2: Location,
765}
766
767#[cfg(test)]
768mod tests {
769 use super::*;
770 use lsp_types::{Position, Range, Url};
771
772 #[test]
773 fn test_route_navigator_new() {
774 let navigator = RouteNavigator::new();
775 assert_eq!(navigator.index.routes.len(), 0);
776 assert_eq!(navigator.index.path_map.len(), 0);
777 }
778
779 #[test]
780 fn test_route_navigator_default() {
781 let navigator = RouteNavigator::default();
782 assert_eq!(navigator.index.routes.len(), 0);
783 assert_eq!(navigator.index.path_map.len(), 0);
784 }
785
786 #[test]
787 fn test_route_index_new() {
788 let index = RouteIndex::new();
789 assert_eq!(index.routes.len(), 0);
790 assert_eq!(index.path_map.len(), 0);
791 }
792
793 #[test]
794 fn test_route_index_default() {
795 let index = RouteIndex::default();
796 assert_eq!(index.routes.len(), 0);
797 assert_eq!(index.path_map.len(), 0);
798 }
799
800 #[test]
801 fn test_route_info_creation() {
802 let route = RouteInfo {
803 path: "/users/{id}".to_string(),
804 methods: vec![HttpMethod::Get],
805 handler: HandlerInfo {
806 function_name: "get_user".to_string(),
807 parameters: vec![Parameter {
808 name: "id".to_string(),
809 type_name: "i64".to_string(),
810 }],
811 },
812 location: Location {
813 uri: Url::parse("file:///test.rs").unwrap(),
814 range: Range {
815 start: Position {
816 line: 10,
817 character: 0,
818 },
819 end: Position {
820 line: 15,
821 character: 0,
822 },
823 },
824 },
825 };
826
827 assert_eq!(route.path, "/users/{id}");
828 assert_eq!(route.methods.len(), 1);
829 assert_eq!(route.methods[0], HttpMethod::Get);
830 assert_eq!(route.handler.function_name, "get_user");
831 assert_eq!(route.handler.parameters.len(), 1);
832 assert_eq!(route.handler.parameters[0].name, "id");
833 assert_eq!(route.handler.parameters[0].type_name, "i64");
834 }
835
836 #[test]
837 fn test_handler_info_creation() {
838 let handler = HandlerInfo {
839 function_name: "create_user".to_string(),
840 parameters: vec![
841 Parameter {
842 name: "body".to_string(),
843 type_name: "Json<CreateUserRequest>".to_string(),
844 },
845 Parameter {
846 name: "db".to_string(),
847 type_name: "Component<ConnectPool>".to_string(),
848 },
849 ],
850 };
851
852 assert_eq!(handler.function_name, "create_user");
853 assert_eq!(handler.parameters.len(), 2);
854 assert_eq!(handler.parameters[0].name, "body");
855 assert_eq!(handler.parameters[1].name, "db");
856 }
857
858 #[test]
859 fn test_parameter_creation() {
860 let param = Parameter {
861 name: "user_id".to_string(),
862 type_name: "Path<i64>".to_string(),
863 };
864
865 assert_eq!(param.name, "user_id");
866 assert_eq!(param.type_name, "Path<i64>");
867 }
868
869 #[test]
870 fn test_route_info_with_multiple_methods() {
871 let route = RouteInfo {
872 path: "/users".to_string(),
873 methods: vec![HttpMethod::Get, HttpMethod::Post],
874 handler: HandlerInfo {
875 function_name: "handle_users".to_string(),
876 parameters: vec![],
877 },
878 location: Location {
879 uri: Url::parse("file:///test.rs").unwrap(),
880 range: Range {
881 start: Position {
882 line: 0,
883 character: 0,
884 },
885 end: Position {
886 line: 0,
887 character: 0,
888 },
889 },
890 },
891 };
892
893 assert_eq!(route.methods.len(), 2);
894 assert!(route.methods.contains(&HttpMethod::Get));
895 assert!(route.methods.contains(&HttpMethod::Post));
896 }
897
898 #[test]
899 fn test_route_info_clone() {
900 let route = RouteInfo {
901 path: "/test".to_string(),
902 methods: vec![HttpMethod::Get],
903 handler: HandlerInfo {
904 function_name: "test_handler".to_string(),
905 parameters: vec![],
906 },
907 location: Location {
908 uri: Url::parse("file:///test.rs").unwrap(),
909 range: Range {
910 start: Position {
911 line: 0,
912 character: 0,
913 },
914 end: Position {
915 line: 0,
916 character: 0,
917 },
918 },
919 },
920 };
921
922 let cloned = route.clone();
923 assert_eq!(route.path, cloned.path);
924 assert_eq!(route.handler.function_name, cloned.handler.function_name);
925 }
926
927 #[test]
928 fn test_route_index_with_routes() {
929 let mut index = RouteIndex::new();
930
931 let route1 = RouteInfo {
933 path: "/users".to_string(),
934 methods: vec![HttpMethod::Get],
935 handler: HandlerInfo {
936 function_name: "list_users".to_string(),
937 parameters: vec![],
938 },
939 location: Location {
940 uri: Url::parse("file:///test.rs").unwrap(),
941 range: Range {
942 start: Position {
943 line: 0,
944 character: 0,
945 },
946 end: Position {
947 line: 0,
948 character: 0,
949 },
950 },
951 },
952 };
953
954 let route2 = RouteInfo {
955 path: "/users/{id}".to_string(),
956 methods: vec![HttpMethod::Get],
957 handler: HandlerInfo {
958 function_name: "get_user".to_string(),
959 parameters: vec![],
960 },
961 location: Location {
962 uri: Url::parse("file:///test.rs").unwrap(),
963 range: Range {
964 start: Position {
965 line: 0,
966 character: 0,
967 },
968 end: Position {
969 line: 0,
970 character: 0,
971 },
972 },
973 },
974 };
975
976 index.routes.push(route1);
977 index.routes.push(route2);
978
979 index.path_map.insert("/users".to_string(), vec![0]);
981 index.path_map.insert("/users/{id}".to_string(), vec![1]);
982
983 assert_eq!(index.routes.len(), 2);
984 assert_eq!(index.path_map.len(), 2);
985 assert_eq!(index.path_map.get("/users"), Some(&vec![0]));
986 assert_eq!(index.path_map.get("/users/{id}"), Some(&vec![1]));
987 }
988
989 #[test]
990 fn test_build_index_empty_documents() {
991 let mut navigator = RouteNavigator::new();
992 let documents = vec![];
993
994 navigator.build_index(&documents);
995
996 assert_eq!(navigator.index.routes.len(), 0);
997 assert_eq!(navigator.index.path_map.len(), 0);
998 }
999
1000 #[test]
1001 fn test_build_index_single_route() {
1002 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1003
1004 let mut navigator = RouteNavigator::new();
1005
1006 let route_macro = RouteMacro {
1007 path: "/users".to_string(),
1008 methods: vec![HttpMethod::Get],
1009 middlewares: vec![],
1010 handler_name: "list_users".to_string(),
1011 range: Range {
1012 start: Position {
1013 line: 10,
1014 character: 0,
1015 },
1016 end: Position {
1017 line: 15,
1018 character: 0,
1019 },
1020 },
1021 };
1022
1023 let doc = RustDocument {
1024 uri: Url::parse("file:///test.rs").unwrap(),
1025 content: String::new(),
1026 macros: vec![SpringMacro::Route(route_macro)],
1027 };
1028
1029 navigator.build_index(&[doc]);
1030
1031 assert_eq!(navigator.index.routes.len(), 1);
1032 assert_eq!(navigator.index.routes[0].path, "/users");
1033 assert_eq!(navigator.index.routes[0].methods.len(), 1);
1034 assert_eq!(navigator.index.routes[0].methods[0], HttpMethod::Get);
1035 assert_eq!(
1036 navigator.index.routes[0].handler.function_name,
1037 "list_users"
1038 );
1039 assert_eq!(navigator.index.path_map.len(), 1);
1040 assert_eq!(navigator.index.path_map.get("/users"), Some(&vec![0]));
1041 }
1042
1043 #[test]
1044 fn test_build_index_with_path_parameters() {
1045 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1046
1047 let mut navigator = RouteNavigator::new();
1048
1049 let route_macro = RouteMacro {
1050 path: "/users/{id}".to_string(),
1051 methods: vec![HttpMethod::Get],
1052 middlewares: vec![],
1053 handler_name: "get_user".to_string(),
1054 range: Range {
1055 start: Position {
1056 line: 10,
1057 character: 0,
1058 },
1059 end: Position {
1060 line: 15,
1061 character: 0,
1062 },
1063 },
1064 };
1065
1066 let doc = RustDocument {
1067 uri: Url::parse("file:///test.rs").unwrap(),
1068 content: String::new(),
1069 macros: vec![SpringMacro::Route(route_macro)],
1070 };
1071
1072 navigator.build_index(&[doc]);
1073
1074 assert_eq!(navigator.index.routes.len(), 1);
1075 assert_eq!(navigator.index.routes[0].path, "/users/{id}");
1076 assert_eq!(navigator.index.routes[0].handler.parameters.len(), 1);
1077 assert_eq!(navigator.index.routes[0].handler.parameters[0].name, "id");
1078 }
1079
1080 #[test]
1081 fn test_build_index_multiple_path_parameters() {
1082 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1083
1084 let mut navigator = RouteNavigator::new();
1085
1086 let route_macro = RouteMacro {
1087 path: "/users/{user_id}/posts/{post_id}".to_string(),
1088 methods: vec![HttpMethod::Get],
1089 middlewares: vec![],
1090 handler_name: "get_user_post".to_string(),
1091 range: Range {
1092 start: Position {
1093 line: 10,
1094 character: 0,
1095 },
1096 end: Position {
1097 line: 15,
1098 character: 0,
1099 },
1100 },
1101 };
1102
1103 let doc = RustDocument {
1104 uri: Url::parse("file:///test.rs").unwrap(),
1105 content: String::new(),
1106 macros: vec![SpringMacro::Route(route_macro)],
1107 };
1108
1109 navigator.build_index(&[doc]);
1110
1111 assert_eq!(navigator.index.routes.len(), 1);
1112 assert_eq!(navigator.index.routes[0].handler.parameters.len(), 2);
1113 assert_eq!(
1114 navigator.index.routes[0].handler.parameters[0].name,
1115 "user_id"
1116 );
1117 assert_eq!(
1118 navigator.index.routes[0].handler.parameters[1].name,
1119 "post_id"
1120 );
1121 }
1122
1123 #[test]
1124 fn test_build_index_multi_method_route() {
1125 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1126
1127 let mut navigator = RouteNavigator::new();
1128
1129 let route_macro = RouteMacro {
1131 path: "/users".to_string(),
1132 methods: vec![HttpMethod::Get, HttpMethod::Post],
1133 middlewares: vec![],
1134 handler_name: "handle_users".to_string(),
1135 range: Range {
1136 start: Position {
1137 line: 10,
1138 character: 0,
1139 },
1140 end: Position {
1141 line: 15,
1142 character: 0,
1143 },
1144 },
1145 };
1146
1147 let doc = RustDocument {
1148 uri: Url::parse("file:///test.rs").unwrap(),
1149 content: String::new(),
1150 macros: vec![SpringMacro::Route(route_macro)],
1151 };
1152
1153 navigator.build_index(&[doc]);
1154
1155 assert_eq!(navigator.index.routes.len(), 2);
1157 assert_eq!(navigator.index.routes[0].methods.len(), 1);
1158 assert_eq!(navigator.index.routes[0].methods[0], HttpMethod::Get);
1159 assert_eq!(navigator.index.routes[1].methods.len(), 1);
1160 assert_eq!(navigator.index.routes[1].methods[0], HttpMethod::Post);
1161
1162 assert_eq!(navigator.index.path_map.get("/users"), Some(&vec![0, 1]));
1164 }
1165
1166 #[test]
1167 fn test_build_index_multiple_routes() {
1168 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1169
1170 let mut navigator = RouteNavigator::new();
1171
1172 let route1 = RouteMacro {
1173 path: "/users".to_string(),
1174 methods: vec![HttpMethod::Get],
1175 middlewares: vec![],
1176 handler_name: "list_users".to_string(),
1177 range: Range {
1178 start: Position {
1179 line: 10,
1180 character: 0,
1181 },
1182 end: Position {
1183 line: 15,
1184 character: 0,
1185 },
1186 },
1187 };
1188
1189 let route2 = RouteMacro {
1190 path: "/users/{id}".to_string(),
1191 methods: vec![HttpMethod::Get],
1192 middlewares: vec![],
1193 handler_name: "get_user".to_string(),
1194 range: Range {
1195 start: Position {
1196 line: 20,
1197 character: 0,
1198 },
1199 end: Position {
1200 line: 25,
1201 character: 0,
1202 },
1203 },
1204 };
1205
1206 let route3 = RouteMacro {
1207 path: "/posts".to_string(),
1208 methods: vec![HttpMethod::Get, HttpMethod::Post],
1209 middlewares: vec![],
1210 handler_name: "handle_posts".to_string(),
1211 range: Range {
1212 start: Position {
1213 line: 30,
1214 character: 0,
1215 },
1216 end: Position {
1217 line: 35,
1218 character: 0,
1219 },
1220 },
1221 };
1222
1223 let doc = RustDocument {
1224 uri: Url::parse("file:///test.rs").unwrap(),
1225 content: String::new(),
1226 macros: vec![
1227 SpringMacro::Route(route1),
1228 SpringMacro::Route(route2),
1229 SpringMacro::Route(route3),
1230 ],
1231 };
1232
1233 navigator.build_index(&[doc]);
1234
1235 assert_eq!(navigator.index.routes.len(), 4);
1237 assert_eq!(navigator.index.path_map.len(), 3);
1238 assert_eq!(navigator.index.path_map.get("/users"), Some(&vec![0]));
1239 assert_eq!(navigator.index.path_map.get("/users/{id}"), Some(&vec![1]));
1240 assert_eq!(navigator.index.path_map.get("/posts"), Some(&vec![2, 3]));
1241 }
1242
1243 #[test]
1244 fn test_build_index_multiple_documents() {
1245 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1246
1247 let mut navigator = RouteNavigator::new();
1248
1249 let route1 = RouteMacro {
1250 path: "/users".to_string(),
1251 methods: vec![HttpMethod::Get],
1252 middlewares: vec![],
1253 handler_name: "list_users".to_string(),
1254 range: Range {
1255 start: Position {
1256 line: 10,
1257 character: 0,
1258 },
1259 end: Position {
1260 line: 15,
1261 character: 0,
1262 },
1263 },
1264 };
1265
1266 let doc1 = RustDocument {
1267 uri: Url::parse("file:///users.rs").unwrap(),
1268 content: String::new(),
1269 macros: vec![SpringMacro::Route(route1)],
1270 };
1271
1272 let route2 = RouteMacro {
1273 path: "/posts".to_string(),
1274 methods: vec![HttpMethod::Get],
1275 middlewares: vec![],
1276 handler_name: "list_posts".to_string(),
1277 range: Range {
1278 start: Position {
1279 line: 10,
1280 character: 0,
1281 },
1282 end: Position {
1283 line: 15,
1284 character: 0,
1285 },
1286 },
1287 };
1288
1289 let doc2 = RustDocument {
1290 uri: Url::parse("file:///posts.rs").unwrap(),
1291 content: String::new(),
1292 macros: vec![SpringMacro::Route(route2)],
1293 };
1294
1295 navigator.build_index(&[doc1, doc2]);
1296
1297 assert_eq!(navigator.index.routes.len(), 2);
1298 assert_eq!(navigator.index.path_map.len(), 2);
1299 }
1300
1301 #[test]
1302 fn test_build_index_rebuild_clears_old_index() {
1303 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1304
1305 let mut navigator = RouteNavigator::new();
1306
1307 let route1 = RouteMacro {
1309 path: "/users".to_string(),
1310 methods: vec![HttpMethod::Get],
1311 middlewares: vec![],
1312 handler_name: "list_users".to_string(),
1313 range: Range {
1314 start: Position {
1315 line: 10,
1316 character: 0,
1317 },
1318 end: Position {
1319 line: 15,
1320 character: 0,
1321 },
1322 },
1323 };
1324
1325 let doc1 = RustDocument {
1326 uri: Url::parse("file:///test.rs").unwrap(),
1327 content: String::new(),
1328 macros: vec![SpringMacro::Route(route1)],
1329 };
1330
1331 navigator.build_index(&[doc1]);
1332 assert_eq!(navigator.index.routes.len(), 1);
1333
1334 let route2 = RouteMacro {
1336 path: "/posts".to_string(),
1337 methods: vec![HttpMethod::Get],
1338 middlewares: vec![],
1339 handler_name: "list_posts".to_string(),
1340 range: Range {
1341 start: Position {
1342 line: 10,
1343 character: 0,
1344 },
1345 end: Position {
1346 line: 15,
1347 character: 0,
1348 },
1349 },
1350 };
1351
1352 let doc2 = RustDocument {
1353 uri: Url::parse("file:///test.rs").unwrap(),
1354 content: String::new(),
1355 macros: vec![SpringMacro::Route(route2)],
1356 };
1357
1358 navigator.build_index(&[doc2]);
1359
1360 assert_eq!(navigator.index.routes.len(), 1);
1362 assert_eq!(navigator.index.routes[0].path, "/posts");
1363 assert_eq!(navigator.index.path_map.len(), 1);
1364 assert!(navigator.index.path_map.contains_key("/posts"));
1365 assert!(!navigator.index.path_map.contains_key("/users"));
1366 }
1367
1368 #[test]
1369 fn test_parse_path_parameters_no_params() {
1370 let navigator = RouteNavigator::new();
1371 let params = navigator.parse_path_parameters("/users");
1372 assert_eq!(params.len(), 0);
1373 }
1374
1375 #[test]
1376 fn test_parse_path_parameters_single_param() {
1377 let navigator = RouteNavigator::new();
1378 let params = navigator.parse_path_parameters("/users/{id}");
1379 assert_eq!(params.len(), 1);
1380 assert_eq!(params[0], "id");
1381 }
1382
1383 #[test]
1384 fn test_parse_path_parameters_multiple_params() {
1385 let navigator = RouteNavigator::new();
1386 let params = navigator.parse_path_parameters("/users/{user_id}/posts/{post_id}");
1387 assert_eq!(params.len(), 2);
1388 assert_eq!(params[0], "user_id");
1389 assert_eq!(params[1], "post_id");
1390 }
1391
1392 #[test]
1393 fn test_parse_path_parameters_empty_param() {
1394 let navigator = RouteNavigator::new();
1395 let params = navigator.parse_path_parameters("/users/{}");
1396 assert_eq!(params.len(), 0);
1398 }
1399
1400 #[test]
1401 fn test_parse_path_parameters_complex_path() {
1402 let navigator = RouteNavigator::new();
1403 let params = navigator
1404 .parse_path_parameters("/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}");
1405 assert_eq!(params.len(), 3);
1406 assert_eq!(params[0], "user_id");
1407 assert_eq!(params[1], "post_id");
1408 assert_eq!(params[2], "comment_id");
1409 }
1410
1411 #[test]
1416 fn test_get_all_routes_empty() {
1417 let navigator = RouteNavigator::new();
1418 let routes = navigator.get_all_routes();
1419 assert_eq!(routes.len(), 0);
1420 }
1421
1422 #[test]
1423 fn test_get_all_routes() {
1424 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1425
1426 let mut navigator = RouteNavigator::new();
1427
1428 let route1 = RouteMacro {
1429 path: "/users".to_string(),
1430 methods: vec![HttpMethod::Get],
1431 middlewares: vec![],
1432 handler_name: "list_users".to_string(),
1433 range: Range {
1434 start: Position {
1435 line: 10,
1436 character: 0,
1437 },
1438 end: Position {
1439 line: 15,
1440 character: 0,
1441 },
1442 },
1443 };
1444
1445 let route2 = RouteMacro {
1446 path: "/posts".to_string(),
1447 methods: vec![HttpMethod::Get],
1448 middlewares: vec![],
1449 handler_name: "list_posts".to_string(),
1450 range: Range {
1451 start: Position {
1452 line: 20,
1453 character: 0,
1454 },
1455 end: Position {
1456 line: 25,
1457 character: 0,
1458 },
1459 },
1460 };
1461
1462 let doc = RustDocument {
1463 uri: Url::parse("file:///test.rs").unwrap(),
1464 content: String::new(),
1465 macros: vec![SpringMacro::Route(route1), SpringMacro::Route(route2)],
1466 };
1467
1468 navigator.build_index(&[doc]);
1469
1470 let routes = navigator.get_all_routes();
1471 assert_eq!(routes.len(), 2);
1472 assert_eq!(routes[0].path, "/users");
1473 assert_eq!(routes[1].path, "/posts");
1474 }
1475
1476 #[test]
1477 fn test_find_routes_empty_pattern() {
1478 let navigator = RouteNavigator::new();
1479 let routes = navigator.find_routes("");
1480 assert_eq!(routes.len(), 0);
1481 }
1482
1483 #[test]
1484 fn test_find_routes_fuzzy_match() {
1485 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1486
1487 let mut navigator = RouteNavigator::new();
1488
1489 let routes_data = vec![
1490 ("/users", "list_users"),
1491 ("/users/{id}", "get_user"),
1492 ("/posts", "list_posts"),
1493 ("/api/users", "api_list_users"),
1494 ];
1495
1496 let macros: Vec<_> = routes_data
1497 .into_iter()
1498 .map(|(path, handler)| {
1499 SpringMacro::Route(RouteMacro {
1500 path: path.to_string(),
1501 methods: vec![HttpMethod::Get],
1502 middlewares: vec![],
1503 handler_name: handler.to_string(),
1504 range: Range {
1505 start: Position {
1506 line: 0,
1507 character: 0,
1508 },
1509 end: Position {
1510 line: 0,
1511 character: 0,
1512 },
1513 },
1514 })
1515 })
1516 .collect();
1517
1518 let doc = RustDocument {
1519 uri: Url::parse("file:///test.rs").unwrap(),
1520 content: String::new(),
1521 macros,
1522 };
1523
1524 navigator.build_index(&[doc]);
1525
1526 let routes = navigator.find_routes("users");
1528 assert_eq!(routes.len(), 3); let routes = navigator.find_routes("posts");
1532 assert_eq!(routes.len(), 1); let routes = navigator.find_routes("/api");
1536 assert_eq!(routes.len(), 1); }
1538
1539 #[test]
1540 fn test_find_routes_regex_match() {
1541 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1542
1543 let mut navigator = RouteNavigator::new();
1544
1545 let routes_data = vec![
1546 ("/users", "list_users"),
1547 ("/users/{id}", "get_user"),
1548 ("/posts", "list_posts"),
1549 ("/api/v1/users", "api_v1_users"),
1550 ("/api/v2/users", "api_v2_users"),
1551 ];
1552
1553 let macros: Vec<_> = routes_data
1554 .into_iter()
1555 .map(|(path, handler)| {
1556 SpringMacro::Route(RouteMacro {
1557 path: path.to_string(),
1558 methods: vec![HttpMethod::Get],
1559 middlewares: vec![],
1560 handler_name: handler.to_string(),
1561 range: Range {
1562 start: Position {
1563 line: 0,
1564 character: 0,
1565 },
1566 end: Position {
1567 line: 0,
1568 character: 0,
1569 },
1570 },
1571 })
1572 })
1573 .collect();
1574
1575 let doc = RustDocument {
1576 uri: Url::parse("file:///test.rs").unwrap(),
1577 content: String::new(),
1578 macros,
1579 };
1580
1581 navigator.build_index(&[doc]);
1582
1583 let routes = navigator.find_routes("regex:^/api/.*");
1585 assert_eq!(routes.len(), 2); let routes = navigator.find_routes("regex:.*\\{.*\\}.*");
1589 assert_eq!(routes.len(), 1); let routes = navigator.find_routes("regex:^/users");
1593 assert_eq!(routes.len(), 2); }
1595
1596 #[test]
1597 fn test_find_routes_regex_invalid() {
1598 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1599
1600 let mut navigator = RouteNavigator::new();
1601
1602 let route = RouteMacro {
1603 path: "/users".to_string(),
1604 methods: vec![HttpMethod::Get],
1605 middlewares: vec![],
1606 handler_name: "list_users".to_string(),
1607 range: Range {
1608 start: Position {
1609 line: 0,
1610 character: 0,
1611 },
1612 end: Position {
1613 line: 0,
1614 character: 0,
1615 },
1616 },
1617 };
1618
1619 let doc = RustDocument {
1620 uri: Url::parse("file:///test.rs").unwrap(),
1621 content: String::new(),
1622 macros: vec![SpringMacro::Route(route)],
1623 };
1624
1625 navigator.build_index(&[doc]);
1626
1627 let routes = navigator.find_routes("regex:[invalid");
1629 assert_eq!(routes.len(), 0);
1630 }
1631
1632 #[test]
1633 fn test_find_routes_no_match() {
1634 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1635
1636 let mut navigator = RouteNavigator::new();
1637
1638 let route = RouteMacro {
1639 path: "/users".to_string(),
1640 methods: vec![HttpMethod::Get],
1641 middlewares: vec![],
1642 handler_name: "list_users".to_string(),
1643 range: Range {
1644 start: Position {
1645 line: 0,
1646 character: 0,
1647 },
1648 end: Position {
1649 line: 0,
1650 character: 0,
1651 },
1652 },
1653 };
1654
1655 let doc = RustDocument {
1656 uri: Url::parse("file:///test.rs").unwrap(),
1657 content: String::new(),
1658 macros: vec![SpringMacro::Route(route)],
1659 };
1660
1661 navigator.build_index(&[doc]);
1662
1663 let routes = navigator.find_routes("posts");
1665 assert_eq!(routes.len(), 0);
1666
1667 let routes = navigator.find_routes("regex:^/api/.*");
1668 assert_eq!(routes.len(), 0);
1669 }
1670
1671 #[test]
1672 fn test_find_routes_by_handler_empty() {
1673 let navigator = RouteNavigator::new();
1674 let routes = navigator.find_routes_by_handler("get_user");
1675 assert_eq!(routes.len(), 0);
1676 }
1677
1678 #[test]
1679 fn test_find_routes_by_handler_single() {
1680 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1681
1682 let mut navigator = RouteNavigator::new();
1683
1684 let route = RouteMacro {
1685 path: "/users/{id}".to_string(),
1686 methods: vec![HttpMethod::Get],
1687 middlewares: vec![],
1688 handler_name: "get_user".to_string(),
1689 range: Range {
1690 start: Position {
1691 line: 10,
1692 character: 0,
1693 },
1694 end: Position {
1695 line: 15,
1696 character: 0,
1697 },
1698 },
1699 };
1700
1701 let doc = RustDocument {
1702 uri: Url::parse("file:///test.rs").unwrap(),
1703 content: String::new(),
1704 macros: vec![SpringMacro::Route(route)],
1705 };
1706
1707 navigator.build_index(&[doc]);
1708
1709 let routes = navigator.find_routes_by_handler("get_user");
1710 assert_eq!(routes.len(), 1);
1711 assert_eq!(routes[0].path, "/users/{id}");
1712 assert_eq!(routes[0].handler.function_name, "get_user");
1713 }
1714
1715 #[test]
1716 fn test_find_routes_by_handler_multiple() {
1717 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1718
1719 let mut navigator = RouteNavigator::new();
1720
1721 let route1 = RouteMacro {
1723 path: "/users".to_string(),
1724 methods: vec![HttpMethod::Get, HttpMethod::Post],
1725 middlewares: vec![],
1726 handler_name: "handle_users".to_string(),
1727 range: Range {
1728 start: Position {
1729 line: 10,
1730 character: 0,
1731 },
1732 end: Position {
1733 line: 15,
1734 character: 0,
1735 },
1736 },
1737 };
1738
1739 let doc = RustDocument {
1740 uri: Url::parse("file:///test.rs").unwrap(),
1741 content: String::new(),
1742 macros: vec![SpringMacro::Route(route1)],
1743 };
1744
1745 navigator.build_index(&[doc]);
1746
1747 let routes = navigator.find_routes_by_handler("handle_users");
1748 assert_eq!(routes.len(), 2); for route in routes {
1751 assert_eq!(route.path, "/users");
1752 assert_eq!(route.handler.function_name, "handle_users");
1753 }
1754 }
1755
1756 #[test]
1757 fn test_find_routes_by_handler_not_found() {
1758 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1759
1760 let mut navigator = RouteNavigator::new();
1761
1762 let route = RouteMacro {
1763 path: "/users".to_string(),
1764 methods: vec![HttpMethod::Get],
1765 middlewares: vec![],
1766 handler_name: "list_users".to_string(),
1767 range: Range {
1768 start: Position {
1769 line: 10,
1770 character: 0,
1771 },
1772 end: Position {
1773 line: 15,
1774 character: 0,
1775 },
1776 },
1777 };
1778
1779 let doc = RustDocument {
1780 uri: Url::parse("file:///test.rs").unwrap(),
1781 content: String::new(),
1782 macros: vec![SpringMacro::Route(route)],
1783 };
1784
1785 navigator.build_index(&[doc]);
1786
1787 let routes = navigator.find_routes_by_handler("get_user");
1788 assert_eq!(routes.len(), 0);
1789 }
1790
1791 #[test]
1792 fn test_route_location_for_jump() {
1793 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1794
1795 let mut navigator = RouteNavigator::new();
1796
1797 let route = RouteMacro {
1798 path: "/users/{id}".to_string(),
1799 methods: vec![HttpMethod::Get],
1800 middlewares: vec![],
1801 handler_name: "get_user".to_string(),
1802 range: Range {
1803 start: Position {
1804 line: 42,
1805 character: 5,
1806 },
1807 end: Position {
1808 line: 50,
1809 character: 10,
1810 },
1811 },
1812 };
1813
1814 let uri = Url::parse("file:///src/handlers/users.rs").unwrap();
1815
1816 let doc = RustDocument {
1817 uri: uri.clone(),
1818 content: String::new(),
1819 macros: vec![SpringMacro::Route(route)],
1820 };
1821
1822 navigator.build_index(&[doc]);
1823
1824 let routes = navigator.find_routes("users");
1825 assert_eq!(routes.len(), 1);
1826
1827 let location = &routes[0].location;
1829 assert_eq!(location.uri, uri);
1830 assert_eq!(location.range.start.line, 42);
1831 assert_eq!(location.range.start.character, 5);
1832 assert_eq!(location.range.end.line, 50);
1833 assert_eq!(location.range.end.character, 10);
1834 }
1835
1836 #[test]
1841 fn test_validate_routes_empty() {
1842 let navigator = RouteNavigator::new();
1843 let diagnostics = navigator.validate_routes();
1844 assert_eq!(diagnostics.len(), 0);
1845 }
1846
1847 #[test]
1848 fn test_validate_path_characters_valid() {
1849 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1850
1851 let mut navigator = RouteNavigator::new();
1852
1853 let route = RouteMacro {
1854 path: "/api/v1/users/{id}/posts".to_string(),
1855 methods: vec![HttpMethod::Get],
1856 middlewares: vec![],
1857 handler_name: "get_user_posts".to_string(),
1858 range: Range {
1859 start: Position {
1860 line: 10,
1861 character: 0,
1862 },
1863 end: Position {
1864 line: 15,
1865 character: 0,
1866 },
1867 },
1868 };
1869
1870 let doc = RustDocument {
1871 uri: Url::parse("file:///test.rs").unwrap(),
1872 content: String::new(),
1873 macros: vec![SpringMacro::Route(route)],
1874 };
1875
1876 navigator.build_index(&[doc]);
1877
1878 let diagnostics = navigator.validate_routes();
1879 assert!(!diagnostics.iter().any(|d| d
1881 .code
1882 .as_ref()
1883 .and_then(|c| match c {
1884 lsp_types::NumberOrString::String(s) => Some(s.as_str()),
1885 _ => None,
1886 })
1887 .map(|s| s == "invalid-path-char")
1888 .unwrap_or(false)));
1889 }
1890
1891 #[test]
1892 fn test_validate_path_characters_invalid() {
1893 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1894
1895 let mut navigator = RouteNavigator::new();
1896
1897 let route = RouteMacro {
1898 path: "/users/<id>".to_string(), methods: vec![HttpMethod::Get],
1900 middlewares: vec![],
1901 handler_name: "get_user".to_string(),
1902 range: Range {
1903 start: Position {
1904 line: 10,
1905 character: 0,
1906 },
1907 end: Position {
1908 line: 15,
1909 character: 0,
1910 },
1911 },
1912 };
1913
1914 let doc = RustDocument {
1915 uri: Url::parse("file:///test.rs").unwrap(),
1916 content: String::new(),
1917 macros: vec![SpringMacro::Route(route)],
1918 };
1919
1920 navigator.build_index(&[doc]);
1921
1922 let diagnostics = navigator.validate_routes();
1923 assert!(diagnostics.iter().any(|d| d
1925 .code
1926 .as_ref()
1927 .and_then(|c| match c {
1928 lsp_types::NumberOrString::String(s) => Some(s.as_str()),
1929 _ => None,
1930 })
1931 .map(|s| s == "invalid-path-char")
1932 .unwrap_or(false)));
1933 }
1934
1935 #[test]
1936 fn test_validate_path_parameter_syntax_valid() {
1937 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1938
1939 let mut navigator = RouteNavigator::new();
1940
1941 let route = RouteMacro {
1942 path: "/users/{id}/posts/{post_id}".to_string(),
1943 methods: vec![HttpMethod::Get],
1944 middlewares: vec![],
1945 handler_name: "get_user_post".to_string(),
1946 range: Range {
1947 start: Position {
1948 line: 10,
1949 character: 0,
1950 },
1951 end: Position {
1952 line: 15,
1953 character: 0,
1954 },
1955 },
1956 };
1957
1958 let doc = RustDocument {
1959 uri: Url::parse("file:///test.rs").unwrap(),
1960 content: String::new(),
1961 macros: vec![SpringMacro::Route(route)],
1962 };
1963
1964 navigator.build_index(&[doc]);
1965
1966 let diagnostics = navigator.validate_routes();
1967 assert!(!diagnostics.iter().any(|d| {
1969 if let Some(lsp_types::NumberOrString::String(code)) = &d.code {
1970 code.contains("path-param") || code.contains("brace")
1971 } else {
1972 false
1973 }
1974 }));
1975 }
1976
1977 #[test]
1978 fn test_validate_path_parameter_syntax_empty_param() {
1979 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
1980
1981 let mut navigator = RouteNavigator::new();
1982
1983 let route = RouteMacro {
1984 path: "/users/{}".to_string(),
1985 methods: vec![HttpMethod::Get],
1986 middlewares: vec![],
1987 handler_name: "get_user".to_string(),
1988 range: Range {
1989 start: Position {
1990 line: 10,
1991 character: 0,
1992 },
1993 end: Position {
1994 line: 15,
1995 character: 0,
1996 },
1997 },
1998 };
1999
2000 let doc = RustDocument {
2001 uri: Url::parse("file:///test.rs").unwrap(),
2002 content: String::new(),
2003 macros: vec![SpringMacro::Route(route)],
2004 };
2005
2006 navigator.build_index(&[doc]);
2007
2008 let diagnostics = navigator.validate_routes();
2009 assert!(diagnostics.iter().any(|d| d
2011 .code
2012 .as_ref()
2013 .and_then(|c| match c {
2014 lsp_types::NumberOrString::String(s) => Some(s.as_str()),
2015 _ => None,
2016 })
2017 .map(|s| s == "empty-path-param")
2018 .unwrap_or(false)));
2019 }
2020
2021 #[test]
2022 fn test_validate_path_parameter_syntax_unclosed() {
2023 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
2024
2025 let mut navigator = RouteNavigator::new();
2026
2027 let route = RouteMacro {
2028 path: "/users/{id".to_string(),
2029 methods: vec![HttpMethod::Get],
2030 middlewares: vec![],
2031 handler_name: "get_user".to_string(),
2032 range: Range {
2033 start: Position {
2034 line: 10,
2035 character: 0,
2036 },
2037 end: Position {
2038 line: 15,
2039 character: 0,
2040 },
2041 },
2042 };
2043
2044 let doc = RustDocument {
2045 uri: Url::parse("file:///test.rs").unwrap(),
2046 content: String::new(),
2047 macros: vec![SpringMacro::Route(route)],
2048 };
2049
2050 navigator.build_index(&[doc]);
2051
2052 let diagnostics = navigator.validate_routes();
2053 assert!(diagnostics.iter().any(|d| d
2055 .code
2056 .as_ref()
2057 .and_then(|c| match c {
2058 lsp_types::NumberOrString::String(s) => Some(s.as_str()),
2059 _ => None,
2060 })
2061 .map(|s| s == "unclosed-path-param")
2062 .unwrap_or(false)));
2063 }
2064
2065 #[test]
2066 fn test_validate_path_parameter_syntax_unmatched_closing() {
2067 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
2068
2069 let mut navigator = RouteNavigator::new();
2070
2071 let route = RouteMacro {
2072 path: "/users/id}".to_string(),
2073 methods: vec![HttpMethod::Get],
2074 middlewares: vec![],
2075 handler_name: "get_user".to_string(),
2076 range: Range {
2077 start: Position {
2078 line: 10,
2079 character: 0,
2080 },
2081 end: Position {
2082 line: 15,
2083 character: 0,
2084 },
2085 },
2086 };
2087
2088 let doc = RustDocument {
2089 uri: Url::parse("file:///test.rs").unwrap(),
2090 content: String::new(),
2091 macros: vec![SpringMacro::Route(route)],
2092 };
2093
2094 navigator.build_index(&[doc]);
2095
2096 let diagnostics = navigator.validate_routes();
2097 assert!(diagnostics.iter().any(|d| d
2099 .code
2100 .as_ref()
2101 .and_then(|c| match c {
2102 lsp_types::NumberOrString::String(s) => Some(s.as_str()),
2103 _ => None,
2104 })
2105 .map(|s| s == "unmatched-closing-brace")
2106 .unwrap_or(false)));
2107 }
2108
2109 #[test]
2110 fn test_validate_path_parameter_syntax_nested() {
2111 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
2112
2113 let mut navigator = RouteNavigator::new();
2114
2115 let route = RouteMacro {
2116 path: "/users/{{id}}".to_string(),
2117 methods: vec![HttpMethod::Get],
2118 middlewares: vec![],
2119 handler_name: "get_user".to_string(),
2120 range: Range {
2121 start: Position {
2122 line: 10,
2123 character: 0,
2124 },
2125 end: Position {
2126 line: 15,
2127 character: 0,
2128 },
2129 },
2130 };
2131
2132 let doc = RustDocument {
2133 uri: Url::parse("file:///test.rs").unwrap(),
2134 content: String::new(),
2135 macros: vec![SpringMacro::Route(route)],
2136 };
2137
2138 navigator.build_index(&[doc]);
2139
2140 let diagnostics = navigator.validate_routes();
2141 assert!(diagnostics.iter().any(|d| d
2143 .code
2144 .as_ref()
2145 .and_then(|c| match c {
2146 lsp_types::NumberOrString::String(s) => Some(s.as_str()),
2147 _ => None,
2148 })
2149 .map(|s| s == "nested-path-param")
2150 .unwrap_or(false)));
2151 }
2152
2153 #[test]
2154 fn test_validate_restful_style_valid() {
2155 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
2156
2157 let mut navigator = RouteNavigator::new();
2158
2159 let route = RouteMacro {
2160 path: "/api/v1/users/{id}/posts".to_string(),
2161 methods: vec![HttpMethod::Get],
2162 middlewares: vec![],
2163 handler_name: "get_user_posts".to_string(),
2164 range: Range {
2165 start: Position {
2166 line: 10,
2167 character: 0,
2168 },
2169 end: Position {
2170 line: 15,
2171 character: 0,
2172 },
2173 },
2174 };
2175
2176 let doc = RustDocument {
2177 uri: Url::parse("file:///test.rs").unwrap(),
2178 content: String::new(),
2179 macros: vec![SpringMacro::Route(route)],
2180 };
2181
2182 navigator.build_index(&[doc]);
2183
2184 let diagnostics = navigator.validate_routes();
2185 assert!(!diagnostics.iter().any(|d| {
2187 if let Some(lsp_types::NumberOrString::String(code)) = &d.code {
2188 code.starts_with("restful-style")
2189 } else {
2190 false
2191 }
2192 }));
2193 }
2194
2195 #[test]
2196 fn test_validate_restful_style_verb() {
2197 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
2198
2199 let mut navigator = RouteNavigator::new();
2200
2201 let route = RouteMacro {
2202 path: "/getUsers".to_string(),
2203 methods: vec![HttpMethod::Get],
2204 middlewares: vec![],
2205 handler_name: "get_users".to_string(),
2206 range: Range {
2207 start: Position {
2208 line: 10,
2209 character: 0,
2210 },
2211 end: Position {
2212 line: 15,
2213 character: 0,
2214 },
2215 },
2216 };
2217
2218 let doc = RustDocument {
2219 uri: Url::parse("file:///test.rs").unwrap(),
2220 content: String::new(),
2221 macros: vec![SpringMacro::Route(route)],
2222 };
2223
2224 navigator.build_index(&[doc]);
2225
2226 let diagnostics = navigator.validate_routes();
2227 assert!(diagnostics.iter().any(|d| d
2229 .code
2230 .as_ref()
2231 .and_then(|c| match c {
2232 lsp_types::NumberOrString::String(s) => Some(s.as_str()),
2233 _ => None,
2234 })
2235 .map(|s| s == "restful-style-verb")
2236 .unwrap_or(false)));
2237 }
2238
2239 #[test]
2240 fn test_validate_restful_style_case() {
2241 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
2242
2243 let mut navigator = RouteNavigator::new();
2244
2245 let route = RouteMacro {
2246 path: "/userProfiles".to_string(),
2247 methods: vec![HttpMethod::Get],
2248 middlewares: vec![],
2249 handler_name: "get_user_profiles".to_string(),
2250 range: Range {
2251 start: Position {
2252 line: 10,
2253 character: 0,
2254 },
2255 end: Position {
2256 line: 15,
2257 character: 0,
2258 },
2259 },
2260 };
2261
2262 let doc = RustDocument {
2263 uri: Url::parse("file:///test.rs").unwrap(),
2264 content: String::new(),
2265 macros: vec![SpringMacro::Route(route)],
2266 };
2267
2268 navigator.build_index(&[doc]);
2269
2270 let diagnostics = navigator.validate_routes();
2271 assert!(diagnostics.iter().any(|d| d
2273 .code
2274 .as_ref()
2275 .and_then(|c| match c {
2276 lsp_types::NumberOrString::String(s) => Some(s.as_str()),
2277 _ => None,
2278 })
2279 .map(|s| s == "restful-style-case")
2280 .unwrap_or(false)));
2281 }
2282
2283 #[test]
2284 fn test_detect_conflicts_empty() {
2285 let navigator = RouteNavigator::new();
2286 let conflicts = navigator.detect_conflicts();
2287 assert_eq!(conflicts.len(), 0);
2288 }
2289
2290 #[test]
2291 fn test_detect_conflicts_no_conflict() {
2292 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
2293
2294 let mut navigator = RouteNavigator::new();
2295
2296 let route1 = RouteMacro {
2297 path: "/users".to_string(),
2298 methods: vec![HttpMethod::Get],
2299 middlewares: vec![],
2300 handler_name: "list_users".to_string(),
2301 range: Range {
2302 start: Position {
2303 line: 10,
2304 character: 0,
2305 },
2306 end: Position {
2307 line: 15,
2308 character: 0,
2309 },
2310 },
2311 };
2312
2313 let route2 = RouteMacro {
2314 path: "/users".to_string(),
2315 methods: vec![HttpMethod::Post],
2316 middlewares: vec![],
2317 handler_name: "create_user".to_string(),
2318 range: Range {
2319 start: Position {
2320 line: 20,
2321 character: 0,
2322 },
2323 end: Position {
2324 line: 25,
2325 character: 0,
2326 },
2327 },
2328 };
2329
2330 let doc = RustDocument {
2331 uri: Url::parse("file:///test.rs").unwrap(),
2332 content: String::new(),
2333 macros: vec![SpringMacro::Route(route1), SpringMacro::Route(route2)],
2334 };
2335
2336 navigator.build_index(&[doc]);
2337
2338 let conflicts = navigator.detect_conflicts();
2339 assert_eq!(conflicts.len(), 0);
2341 }
2342
2343 #[test]
2344 fn test_detect_conflicts_same_path_and_method() {
2345 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
2346
2347 let mut navigator = RouteNavigator::new();
2348
2349 let route1 = RouteMacro {
2350 path: "/users".to_string(),
2351 methods: vec![HttpMethod::Get],
2352 middlewares: vec![],
2353 handler_name: "list_users".to_string(),
2354 range: Range {
2355 start: Position {
2356 line: 10,
2357 character: 0,
2358 },
2359 end: Position {
2360 line: 15,
2361 character: 0,
2362 },
2363 },
2364 };
2365
2366 let route2 = RouteMacro {
2367 path: "/users".to_string(),
2368 methods: vec![HttpMethod::Get],
2369 middlewares: vec![],
2370 handler_name: "get_users".to_string(),
2371 range: Range {
2372 start: Position {
2373 line: 20,
2374 character: 0,
2375 },
2376 end: Position {
2377 line: 25,
2378 character: 0,
2379 },
2380 },
2381 };
2382
2383 let doc = RustDocument {
2384 uri: Url::parse("file:///test.rs").unwrap(),
2385 content: String::new(),
2386 macros: vec![SpringMacro::Route(route1), SpringMacro::Route(route2)],
2387 };
2388
2389 navigator.build_index(&[doc]);
2390
2391 let conflicts = navigator.detect_conflicts();
2392 assert_eq!(conflicts.len(), 1);
2394 assert_eq!(conflicts[0].path, "/users");
2395 assert_eq!(conflicts[0].method, HttpMethod::Get);
2396 }
2397
2398 #[test]
2399 fn test_detect_conflicts_multiple() {
2400 use crate::macro_analyzer::{RouteMacro, RustDocument, SpringMacro};
2401
2402 let mut navigator = RouteNavigator::new();
2403
2404 let route1 = RouteMacro {
2406 path: "/users".to_string(),
2407 methods: vec![HttpMethod::Get],
2408 middlewares: vec![],
2409 handler_name: "handler1".to_string(),
2410 range: Range {
2411 start: Position {
2412 line: 10,
2413 character: 0,
2414 },
2415 end: Position {
2416 line: 15,
2417 character: 0,
2418 },
2419 },
2420 };
2421
2422 let route2 = RouteMacro {
2423 path: "/users".to_string(),
2424 methods: vec![HttpMethod::Get],
2425 middlewares: vec![],
2426 handler_name: "handler2".to_string(),
2427 range: Range {
2428 start: Position {
2429 line: 20,
2430 character: 0,
2431 },
2432 end: Position {
2433 line: 25,
2434 character: 0,
2435 },
2436 },
2437 };
2438
2439 let route3 = RouteMacro {
2440 path: "/users".to_string(),
2441 methods: vec![HttpMethod::Get],
2442 middlewares: vec![],
2443 handler_name: "handler3".to_string(),
2444 range: Range {
2445 start: Position {
2446 line: 30,
2447 character: 0,
2448 },
2449 end: Position {
2450 line: 35,
2451 character: 0,
2452 },
2453 },
2454 };
2455
2456 let doc = RustDocument {
2457 uri: Url::parse("file:///test.rs").unwrap(),
2458 content: String::new(),
2459 macros: vec![
2460 SpringMacro::Route(route1),
2461 SpringMacro::Route(route2),
2462 SpringMacro::Route(route3),
2463 ],
2464 };
2465
2466 navigator.build_index(&[doc]);
2467
2468 let conflicts = navigator.detect_conflicts();
2469 assert_eq!(conflicts.len(), 3);
2471 }
2472
2473 #[test]
2474 fn test_route_conflict_creation() {
2475 let conflict = RouteConflict {
2476 index1: 0,
2477 index2: 1,
2478 path: "/users".to_string(),
2479 method: HttpMethod::Get,
2480 location1: Location {
2481 uri: Url::parse("file:///test.rs").unwrap(),
2482 range: Range {
2483 start: Position {
2484 line: 10,
2485 character: 0,
2486 },
2487 end: Position {
2488 line: 15,
2489 character: 0,
2490 },
2491 },
2492 },
2493 location2: Location {
2494 uri: Url::parse("file:///test.rs").unwrap(),
2495 range: Range {
2496 start: Position {
2497 line: 20,
2498 character: 0,
2499 },
2500 end: Position {
2501 line: 25,
2502 character: 0,
2503 },
2504 },
2505 },
2506 };
2507
2508 assert_eq!(conflict.index1, 0);
2509 assert_eq!(conflict.index2, 1);
2510 assert_eq!(conflict.path, "/users");
2511 assert_eq!(conflict.method, HttpMethod::Get);
2512 }
2513}