1use std::collections::HashMap;
53
54use serde::{Deserialize, Serialize};
55
56use crate::router::ParsedPath;
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
66#[serde(rename_all = "UPPERCASE")]
67pub enum HttpMethod {
68 GET,
70 POST,
72 PUT,
74 DELETE,
76 PATCH,
78 OPTIONS,
80}
81
82impl HttpMethod {
83 pub fn parse(s: &str) -> Result<Self, RouteConfigError> {
93 match s.to_uppercase().as_str() {
94 "GET" => Ok(HttpMethod::GET),
95 "POST" => Ok(HttpMethod::POST),
96 "PUT" => Ok(HttpMethod::PUT),
97 "DELETE" => Ok(HttpMethod::DELETE),
98 "PATCH" => Ok(HttpMethod::PATCH),
99 "OPTIONS" => Ok(HttpMethod::OPTIONS),
100 other => Err(RouteConfigError::InvalidMethod(other.to_string())),
101 }
102 }
103
104 pub fn to_axum_method(&self) -> axum::http::Method {
106 match self {
107 HttpMethod::GET => axum::http::Method::GET,
108 HttpMethod::POST => axum::http::Method::POST,
109 HttpMethod::PUT => axum::http::Method::PUT,
110 HttpMethod::DELETE => axum::http::Method::DELETE,
111 HttpMethod::PATCH => axum::http::Method::PATCH,
112 HttpMethod::OPTIONS => axum::http::Method::OPTIONS,
113 }
114 }
115}
116
117impl std::fmt::Display for HttpMethod {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 HttpMethod::GET => write!(f, "GET"),
121 HttpMethod::POST => write!(f, "POST"),
122 HttpMethod::PUT => write!(f, "PUT"),
123 HttpMethod::DELETE => write!(f, "DELETE"),
124 HttpMethod::PATCH => write!(f, "PATCH"),
125 HttpMethod::OPTIONS => write!(f, "OPTIONS"),
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct HandlerRef {
141 pub controller: String,
143 pub action: String,
145}
146
147impl HandlerRef {
148 pub fn parse(s: &str) -> Result<Self, RouteConfigError> {
167 let s = s.trim();
168 if s.is_empty() {
169 return Err(RouteConfigError::EmptyHandler);
170 }
171
172 let (controller, action) = if let Some((c, a)) = s.split_once('@') {
174 (c, a)
175 } else if let Some((c, a)) = s.split_once('/') {
176 (c, a)
177 } else {
178 (s, crate::router::DEFAULT_ACTION)
179 };
180
181 let controller = controller.trim();
182 let action = action.trim();
183
184 if controller.is_empty() {
185 return Err(RouteConfigError::EmptyController);
186 }
187 if action.is_empty() {
188 return Err(RouteConfigError::EmptyAction);
189 }
190 if !is_valid_identifier(controller) {
191 return Err(RouteConfigError::InvalidController(controller.to_string()));
192 }
193 if !is_valid_identifier(action) {
194 return Err(RouteConfigError::InvalidAction(action.to_string()));
195 }
196
197 Ok(Self {
198 controller: controller.to_string(),
199 action: action.to_string(),
200 })
201 }
202
203 pub fn to_handler_string(&self) -> String {
205 format!("{}@{}", self.controller, self.action)
206 }
207}
208
209fn is_valid_identifier(s: &str) -> bool {
214 let mut chars = s.chars();
215 match chars.next() {
216 Some(first) if first.is_ascii_alphabetic() || first == '_' => {}
217 _ => return false,
218 }
219 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
220}
221
222impl std::fmt::Display for HandlerRef {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 write!(f, "{}@{}", self.controller, self.action)
225 }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub struct HandlerRefRef<'a> {
251 pub controller: &'a str,
253 pub action: &'a str,
255}
256
257impl<'a> HandlerRefRef<'a> {
258 pub fn parse(s: &'a str) -> Result<Self, RouteConfigError> {
263 let s = s.trim();
264 if s.is_empty() {
265 return Err(RouteConfigError::EmptyHandler);
266 }
267
268 let (controller, action) = if let Some((c, a)) = s.split_once('@') {
270 (c, a)
271 } else if let Some((c, a)) = s.split_once('/') {
272 (c, a)
273 } else {
274 (s, crate::router::DEFAULT_ACTION)
275 };
276
277 let controller = controller.trim();
278 let action = action.trim();
279
280 if controller.is_empty() {
281 return Err(RouteConfigError::EmptyController);
282 }
283 if action.is_empty() {
284 return Err(RouteConfigError::EmptyAction);
285 }
286 if !is_valid_identifier(controller) {
287 return Err(RouteConfigError::InvalidController(controller.to_string()));
288 }
289 if !is_valid_identifier(action) {
290 return Err(RouteConfigError::InvalidAction(action.to_string()));
291 }
292
293 Ok(Self { controller, action })
294 }
295
296 pub fn to_owned(&self) -> HandlerRef {
298 HandlerRef {
299 controller: self.controller.to_string(),
300 action: self.action.to_string(),
301 }
302 }
303
304 pub fn to_handler_string(&self) -> String {
306 format!("{}@{}", self.controller, self.action)
307 }
308}
309
310impl<'a> From<HandlerRefRef<'a>> for HandlerRef {
311 fn from(h: HandlerRefRef<'a>) -> Self {
312 h.to_owned()
313 }
314}
315
316impl<'a> std::fmt::Display for HandlerRefRef<'a> {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 write!(f, "{}@{}", self.controller, self.action)
319 }
320}
321
322#[derive(Debug, thiserror::Error)]
328pub enum RouteConfigError {
329 #[error("YAML parse error: {0}")]
331 YamlParse(#[from] serde_yaml::Error),
332
333 #[error("JSON parse error: {0}")]
335 JsonParse(#[from] serde_json::Error),
336
337 #[error("invalid HTTP method: {0}")]
339 InvalidMethod(String),
340
341 #[error("empty handler string")]
343 EmptyHandler,
344
345 #[error("empty controller name in handler")]
347 EmptyController,
348
349 #[error("empty action name in handler")]
351 EmptyAction,
352
353 #[error("invalid controller name: {0}")]
355 InvalidController(String),
356
357 #[error("invalid action name: {0}")]
359 InvalidAction(String),
360
361 #[error("handler parse error: {0}")]
363 HandlerParse(String),
364
365 #[error("route conflict: {method} {path} already registered")]
367 Conflict {
368 method: String,
370 path: String,
372 },
373
374 #[error("failed to read route config file: {0}")]
376 FileRead(#[source] std::io::Error),
377}
378
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383pub struct RouteRule {
384 pub method: HttpMethod,
386 pub path: String,
388 pub handler: String,
390 #[serde(default)]
392 pub middleware: Vec<String>,
393 #[serde(default)]
395 pub name: Option<String>,
396}
397
398impl RouteRule {
399 pub fn new(method: HttpMethod, path: impl Into<String>, handler: impl Into<String>) -> Self {
401 Self {
402 method,
403 path: path.into(),
404 handler: handler.into(),
405 middleware: Vec::new(),
406 name: None,
407 }
408 }
409
410 pub fn handler_ref(&self) -> Result<HandlerRef, RouteConfigError> {
412 HandlerRef::parse(&self.handler)
413 }
414
415 pub fn with_middleware(mut self, name: impl Into<String>) -> Self {
417 self.middleware.push(name.into());
418 self
419 }
420
421 pub fn with_name(mut self, name: impl Into<String>) -> Self {
423 self.name = Some(name.into());
424 self
425 }
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
432pub struct RouteConfig {
433 #[serde(default)]
435 pub routes: Vec<RouteRule>,
436 #[serde(default)]
438 pub groups: Vec<RouteGroup>,
439}
440
441impl RouteConfig {
442 pub fn new() -> Self {
444 Self::default()
445 }
446
447 pub fn add_route(&mut self, rule: RouteRule) {
449 self.routes.push(rule);
450 }
451
452 pub fn add_group(&mut self, group: RouteGroup) {
454 self.groups.push(group);
455 }
456
457 pub fn flatten(&self) -> Vec<RouteRule> {
462 let mut result = self.routes.clone();
463 for group in &self.groups {
464 for rule in &group.routes {
465 let mut flattened = rule.clone();
466 flattened.path = join_path(&group.prefix, &flattened.path);
467 let mut mw = group.middleware.clone();
469 mw.extend(flattened.middleware);
470 flattened.middleware = mw;
471 result.push(flattened);
472 }
473 }
474 result
475 }
476
477 pub fn find_conflicts(&self) -> Vec<(RouteRule, RouteRule)> {
481 let flattened = self.flatten();
482 let mut seen: HashMap<(String, String), usize> = HashMap::new();
483 let mut conflicts = Vec::new();
484
485 for (i, rule) in flattened.iter().enumerate() {
486 let key = (rule.method.to_string(), rule.path.clone());
487 if let Some(&prev_idx) = seen.get(&key) {
488 conflicts.push((flattened[prev_idx].clone(), flattened[i].clone()));
489 } else {
490 seen.insert(key, i);
491 }
492 }
493
494 conflicts
495 }
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
502pub struct RouteGroup {
503 pub prefix: String,
505 #[serde(default)]
507 pub routes: Vec<RouteRule>,
508 #[serde(default)]
510 pub middleware: Vec<String>,
511}
512
513impl RouteGroup {
514 pub fn new(prefix: impl Into<String>) -> Self {
516 Self {
517 prefix: prefix.into(),
518 routes: Vec::new(),
519 middleware: Vec::new(),
520 }
521 }
522
523 pub fn add_route(&mut self, rule: RouteRule) -> &mut Self {
525 self.routes.push(rule);
526 self
527 }
528
529 pub fn with_middleware(mut self, name: impl Into<String>) -> Self {
531 self.middleware.push(name.into());
532 self
533 }
534}
535
536fn join_path(prefix: &str, path: &str) -> String {
538 let prefix = prefix.trim_end_matches('/');
539 let path = path.trim_start_matches('/');
540 if path.is_empty() {
541 prefix.to_string()
542 } else if prefix.is_empty() {
543 format!("/{path}")
544 } else {
545 format!("{prefix}/{path}")
546 }
547}
548
549#[tracing::instrument]
574pub fn load_routes_from_yaml_str(yaml: &str) -> Result<RouteConfig, RouteConfigError> {
575 let config: RouteConfig = serde_yaml::from_str(yaml)?;
576 Ok(config)
577}
578
579#[tracing::instrument]
581pub fn load_routes_from_json_str(json: &str) -> Result<RouteConfig, RouteConfigError> {
582 let config: RouteConfig = serde_json::from_str(json)?;
583 Ok(config)
584}
585
586#[tracing::instrument(skip(path))]
588pub async fn load_routes_from_yaml_file(
589 path: impl AsRef<std::path::Path>,
590) -> Result<RouteConfig, RouteConfigError> {
591 let content = tokio::fs::read_to_string(path)
592 .await
593 .map_err(RouteConfigError::FileRead)?;
594 load_routes_from_yaml_str(&content)
595}
596
597#[tracing::instrument(skip(path))]
599pub async fn load_routes_from_json_file(
600 path: impl AsRef<std::path::Path>,
601) -> Result<RouteConfig, RouteConfigError> {
602 let content = tokio::fs::read_to_string(path)
603 .await
604 .map_err(RouteConfigError::FileRead)?;
605 load_routes_from_json_str(&content)
606}
607
608pub trait ControllerRouter {
633 fn router_rules(&self) -> Vec<RouteRule>;
635
636 fn router_prefix(&self) -> &str {
638 ""
639 }
640
641 fn router_middleware(&self) -> Vec<String> {
643 Vec::new()
644 }
645}
646
647#[derive(Debug, Clone, PartialEq, Eq)]
656pub struct ConventionRoute {
657 pub app: String,
659 pub controller: String,
661 pub action: String,
663 pub method: HttpMethod,
665 pub path: String,
667}
668
669impl ConventionRoute {
670 pub fn from_uri(uri: &str) -> Option<Self> {
682 let parsed = crate::router::parse_path(uri);
683 if parsed.app == crate::router::DEFAULT_APP
685 && parsed.controller == crate::router::DEFAULT_CONTROLLER
686 && parsed.action == crate::router::DEFAULT_ACTION
687 {
688 return None;
689 }
690 let path = format!(
691 "/{}/{}/{}",
692 parsed.app,
693 parsed.controller.to_lowercase(),
694 parsed.action
695 );
696 Some(Self {
697 app: parsed.app.into_owned(),
698 controller: parsed.controller.into_owned(),
699 action: parsed.action.into_owned(),
700 method: HttpMethod::GET,
701 path,
702 })
703 }
704
705 pub fn from_parsed<'a>(parsed: ParsedPath<'a>) -> Option<Self> {
707 let uri = format!(
708 "/{}/{}/{}",
709 parsed.app,
710 parsed.controller.to_lowercase(),
711 parsed.action
712 );
713 Self::from_uri(&uri)
714 }
715}
716
717#[derive(Debug, Clone, Default)]
730pub struct RouteRegistry {
731 pub attribute_routes: Vec<RouteRule>,
733 pub config_routes: Vec<RouteRule>,
735 pub convention_routes: Vec<ConventionRoute>,
737}
738
739impl RouteRegistry {
740 pub fn new() -> Self {
742 Self::default()
743 }
744
745 pub fn add_attribute_route(&mut self, rule: RouteRule) -> &mut Self {
747 self.attribute_routes.push(rule);
748 self
749 }
750
751 pub fn add_attribute_routes(
753 &mut self,
754 rules: impl IntoIterator<Item = RouteRule>,
755 ) -> &mut Self {
756 self.attribute_routes.extend(rules);
757 self
758 }
759
760 pub fn add_config_routes(&mut self, config: &RouteConfig) -> &mut Self {
762 self.config_routes.extend(config.flatten());
763 self
764 }
765
766 pub fn add_convention_route(&mut self, route: ConventionRoute) -> &mut Self {
768 self.convention_routes.push(route);
769 self
770 }
771
772 #[tracing::instrument(skip(self))]
776 pub fn convention_as_rules(&self) -> Vec<RouteRule> {
777 self.convention_routes
778 .iter()
779 .map(|c| RouteRule {
780 method: c.method.clone(),
781 path: c.path.clone(),
782 handler: format!("{}@{}", c.controller, c.action),
783 middleware: Vec::new(),
784 name: Some(format!(
785 "convention.{}.{}.{}",
786 c.app, c.controller, c.action
787 )),
788 })
789 .collect()
790 }
791
792 #[tracing::instrument(skip(self))]
796 pub fn merged_rules(&self) -> Vec<RouteRule> {
797 let mut seen: HashMap<(String, String), RouteRule> = HashMap::new();
798
799 for rule in self.convention_as_rules() {
801 let key = (rule.method.to_string(), rule.path.clone());
802 seen.insert(key, rule);
803 }
804 for rule in &self.config_routes {
805 let key = (rule.method.to_string(), rule.path.clone());
806 seen.insert(key, rule.clone());
807 }
808 for rule in &self.attribute_routes {
809 let key = (rule.method.to_string(), rule.path.clone());
810 seen.insert(key, rule.clone());
811 }
812
813 seen.into_values().collect()
814 }
815
816 pub fn attribute_conflicts(&self) -> Vec<(RouteRule, RouteRule)> {
818 find_conflicts_in(&self.attribute_routes)
819 }
820
821 pub fn config_conflicts(&self) -> Vec<(RouteRule, RouteRule)> {
823 find_conflicts_in(&self.config_routes)
824 }
825
826 pub fn total_count(&self) -> usize {
828 self.attribute_routes.len() + self.config_routes.len() + self.convention_routes.len()
829 }
830}
831
832fn find_conflicts_in(rules: &[RouteRule]) -> Vec<(RouteRule, RouteRule)> {
834 let mut seen: HashMap<(String, String), usize> = HashMap::new();
835 let mut conflicts = Vec::new();
836
837 for (i, rule) in rules.iter().enumerate() {
838 let key = (rule.method.to_string(), rule.path.clone());
839 if let Some(&prev_idx) = seen.get(&key) {
840 conflicts.push((rules[prev_idx].clone(), rules[i].clone()));
841 } else {
842 seen.insert(key, i);
843 }
844 }
845
846 conflicts
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852
853 #[test]
858 fn test_http_method_parse_uppercase() {
859 assert_eq!(HttpMethod::parse("GET").unwrap(), HttpMethod::GET);
860 assert_eq!(HttpMethod::parse("POST").unwrap(), HttpMethod::POST);
861 assert_eq!(HttpMethod::parse("PUT").unwrap(), HttpMethod::PUT);
862 assert_eq!(HttpMethod::parse("DELETE").unwrap(), HttpMethod::DELETE);
863 assert_eq!(HttpMethod::parse("PATCH").unwrap(), HttpMethod::PATCH);
864 assert_eq!(HttpMethod::parse("OPTIONS").unwrap(), HttpMethod::OPTIONS);
865 }
866
867 #[test]
868 fn test_http_method_parse_lowercase() {
869 assert_eq!(HttpMethod::parse("get").unwrap(), HttpMethod::GET);
870 assert_eq!(HttpMethod::parse("post").unwrap(), HttpMethod::POST);
871 }
872
873 #[test]
874 fn test_http_method_parse_mixed_case() {
875 assert_eq!(HttpMethod::parse("Get").unwrap(), HttpMethod::GET);
876 assert_eq!(HttpMethod::parse("pOsT").unwrap(), HttpMethod::POST);
877 }
878
879 #[test]
880 fn test_http_method_parse_invalid() {
881 assert!(HttpMethod::parse("invalid").is_err());
882 assert!(HttpMethod::parse("").is_err());
883 assert!(HttpMethod::parse("CONNECT").is_err());
884 assert!(HttpMethod::parse("TRACE").is_err());
885 }
886
887 #[test]
888 fn test_http_method_to_axum() {
889 assert_eq!(HttpMethod::GET.to_axum_method(), axum::http::Method::GET);
890 assert_eq!(HttpMethod::POST.to_axum_method(), axum::http::Method::POST);
891 assert_eq!(HttpMethod::PUT.to_axum_method(), axum::http::Method::PUT);
892 assert_eq!(
893 HttpMethod::DELETE.to_axum_method(),
894 axum::http::Method::DELETE
895 );
896 assert_eq!(
897 HttpMethod::PATCH.to_axum_method(),
898 axum::http::Method::PATCH
899 );
900 assert_eq!(
901 HttpMethod::OPTIONS.to_axum_method(),
902 axum::http::Method::OPTIONS
903 );
904 }
905
906 #[test]
907 fn test_http_method_display() {
908 assert_eq!(HttpMethod::GET.to_string(), "GET");
909 assert_eq!(HttpMethod::POST.to_string(), "POST");
910 assert_eq!(HttpMethod::PUT.to_string(), "PUT");
911 }
912
913 #[test]
914 fn test_http_method_serde() {
915 let json = serde_json::to_string(&HttpMethod::GET).unwrap();
916 assert_eq!(json, "\"GET\"");
917
918 let m: HttpMethod = serde_json::from_str("\"POST\"").unwrap();
919 assert_eq!(m, HttpMethod::POST);
920 }
921
922 #[test]
927 fn test_handler_ref_parse_at_separator() {
928 let h = HandlerRef::parse("User@list").unwrap();
929 assert_eq!(h.controller, "User");
930 assert_eq!(h.action, "list");
931 }
932
933 #[test]
934 fn test_handler_ref_parse_slash_separator() {
935 let h = HandlerRef::parse("User/list").unwrap();
936 assert_eq!(h.controller, "User");
937 assert_eq!(h.action, "list");
938 }
939
940 #[test]
941 fn test_handler_ref_parse_only_controller() {
942 let h = HandlerRef::parse("User").unwrap();
943 assert_eq!(h.controller, "User");
944 assert_eq!(h.action, "index"); }
946
947 #[test]
948 fn test_handler_ref_parse_with_whitespace() {
949 let h = HandlerRef::parse(" User @ list ").unwrap();
950 assert_eq!(h.controller, "User");
951 assert_eq!(h.action, "list");
952 }
953
954 #[test]
955 fn test_handler_ref_parse_empty() {
956 assert!(HandlerRef::parse("").is_err());
957 assert!(HandlerRef::parse(" ").is_err());
958 }
959
960 #[test]
961 fn test_handler_ref_parse_empty_controller() {
962 assert!(HandlerRef::parse("@list").is_err());
963 assert!(HandlerRef::parse("/list").is_err());
964 }
965
966 #[test]
967 fn test_handler_ref_parse_empty_action() {
968 assert!(HandlerRef::parse("User@").is_err());
969 assert!(HandlerRef::parse("User/").is_err());
970 }
971
972 #[test]
973 fn test_handler_ref_to_string() {
974 let h = HandlerRef {
975 controller: "User".to_string(),
976 action: "list".to_string(),
977 };
978 assert_eq!(h.to_string(), "User@list");
979 }
980
981 #[test]
986 fn test_handler_ref_ref_parse_at_separator() {
987 let h = HandlerRefRef::parse("User@list").unwrap();
988 assert_eq!(h.controller, "User");
989 assert_eq!(h.action, "list");
990 }
991
992 #[test]
993 fn test_handler_ref_ref_parse_slash_separator() {
994 let h = HandlerRefRef::parse("User/list").unwrap();
995 assert_eq!(h.controller, "User");
996 assert_eq!(h.action, "list");
997 }
998
999 #[test]
1000 fn test_handler_ref_ref_parse_only_controller() {
1001 let h = HandlerRefRef::parse("User").unwrap();
1002 assert_eq!(h.controller, "User");
1003 assert_eq!(h.action, "index");
1004 }
1005
1006 #[test]
1007 fn test_handler_ref_ref_parse_with_whitespace() {
1008 let h = HandlerRefRef::parse(" User @ list ").unwrap();
1009 assert_eq!(h.controller, "User");
1010 assert_eq!(h.action, "list");
1011 }
1012
1013 #[test]
1014 fn test_handler_ref_ref_parse_empty() {
1015 assert!(HandlerRefRef::parse("").is_err());
1016 assert!(HandlerRefRef::parse(" ").is_err());
1017 }
1018
1019 #[test]
1020 fn test_handler_ref_ref_parse_empty_controller() {
1021 assert!(HandlerRefRef::parse("@list").is_err());
1022 assert!(HandlerRefRef::parse("/list").is_err());
1023 }
1024
1025 #[test]
1026 fn test_handler_ref_ref_parse_empty_action() {
1027 assert!(HandlerRefRef::parse("User@").is_err());
1028 assert!(HandlerRefRef::parse("User/").is_err());
1029 }
1030
1031 #[test]
1032 fn test_handler_ref_ref_parse_rejects_path_traversal() {
1033 assert!(HandlerRefRef::parse("../Secret@admin").is_err());
1034 }
1035
1036 #[test]
1037 fn test_handler_ref_ref_parse_rejects_special_chars() {
1038 assert!(HandlerRefRef::parse("User$@list").is_err());
1039 }
1040
1041 #[test]
1042 fn test_handler_ref_ref_to_owned_consistency() {
1043 let ref_ref = HandlerRefRef::parse("User@list").unwrap();
1044 let owned = ref_ref.to_owned();
1045 assert_eq!(owned.controller, "User");
1046 assert_eq!(owned.action, "list");
1047 }
1048
1049 #[test]
1050 fn test_handler_ref_ref_from_into_handler_ref() {
1051 let ref_ref = HandlerRefRef::parse("Admin@dashboard").unwrap();
1052 let owned: HandlerRef = ref_ref.into();
1053 assert_eq!(owned.controller, "Admin");
1054 assert_eq!(owned.action, "dashboard");
1055 }
1056
1057 #[test]
1058 fn test_handler_ref_ref_display() {
1059 let h = HandlerRefRef::parse("User@list").unwrap();
1060 assert_eq!(h.to_string(), "User@list");
1061 }
1062
1063 #[test]
1064 fn test_handler_ref_ref_to_handler_string() {
1065 let h = HandlerRefRef::parse("User@list").unwrap();
1066 assert_eq!(h.to_handler_string(), "User@list");
1067 }
1068
1069 #[test]
1074 fn test_handler_ref_parse_rejects_path_traversal() {
1075 assert!(matches!(
1077 HandlerRef::parse("../Secret@admin"),
1078 Err(RouteConfigError::InvalidController(_))
1079 ));
1080 assert!(matches!(
1082 HandlerRef::parse("..@admin"),
1083 Err(RouteConfigError::InvalidController(_))
1084 ));
1085 assert!(matches!(
1087 HandlerRef::parse("User@../evil"),
1088 Err(RouteConfigError::InvalidAction(_))
1089 ));
1090 }
1091
1092 #[test]
1093 fn test_handler_ref_parse_rejects_double_at() {
1094 assert!(matches!(
1097 HandlerRef::parse("User@list@extra"),
1098 Err(RouteConfigError::InvalidAction(_))
1099 ));
1100 }
1101
1102 #[test]
1103 fn test_handler_ref_parse_rejects_space_injection() {
1104 assert!(matches!(
1106 HandlerRef::parse("Us er@list"),
1107 Err(RouteConfigError::InvalidController(_))
1108 ));
1109 assert!(matches!(
1110 HandlerRef::parse("User@li st"),
1111 Err(RouteConfigError::InvalidAction(_))
1112 ));
1113 }
1114
1115 #[test]
1116 fn test_handler_ref_parse_rejects_leading_digit() {
1117 assert!(matches!(
1119 HandlerRef::parse("1User@list"),
1120 Err(RouteConfigError::InvalidController(_))
1121 ));
1122 assert!(matches!(
1123 HandlerRef::parse("User@1list"),
1124 Err(RouteConfigError::InvalidAction(_))
1125 ));
1126 }
1127
1128 #[test]
1129 fn test_handler_ref_parse_accepts_underscore_and_alphanumeric() {
1130 let h = HandlerRef::parse("_Private@_index").unwrap();
1131 assert_eq!(h.controller, "_Private");
1132 assert_eq!(h.action, "_index");
1133
1134 let h = HandlerRef::parse("User@action_1").unwrap();
1135 assert_eq!(h.controller, "User");
1136 assert_eq!(h.action, "action_1");
1137
1138 let h = HandlerRef::parse("CustomerList@getListById").unwrap();
1140 assert_eq!(h.controller, "CustomerList");
1141 assert_eq!(h.action, "getListById");
1142 }
1143
1144 #[test]
1145 fn test_handler_ref_parse_rejects_special_chars() {
1146 assert!(HandlerRef::parse("User:list@action").is_err());
1148 assert!(HandlerRef::parse("User;list@action").is_err());
1149 assert!(HandlerRef::parse(r"User\list@action").is_err());
1150 assert!(HandlerRef::parse("User@act\nion").is_err());
1151 }
1152
1153 #[test]
1158 fn test_route_rule_new() {
1159 let rule = RouteRule::new(HttpMethod::GET, "/users", "User@list");
1160 assert_eq!(rule.method, HttpMethod::GET);
1161 assert_eq!(rule.path, "/users");
1162 assert_eq!(rule.handler, "User@list");
1163 assert!(rule.middleware.is_empty());
1164 assert!(rule.name.is_none());
1165 }
1166
1167 #[test]
1168 fn test_route_rule_handler_ref() {
1169 let rule = RouteRule::new(HttpMethod::GET, "/users", "User@list");
1170 let h = rule.handler_ref().unwrap();
1171 assert_eq!(h.controller, "User");
1172 assert_eq!(h.action, "list");
1173 }
1174
1175 #[test]
1176 fn test_route_rule_with_middleware() {
1177 let rule = RouteRule::new(HttpMethod::GET, "/users", "User@list")
1178 .with_middleware("auth")
1179 .with_middleware("log");
1180 assert_eq!(rule.middleware, vec!["auth", "log"]);
1181 }
1182
1183 #[test]
1184 fn test_route_rule_with_name() {
1185 let rule = RouteRule::new(HttpMethod::GET, "/users", "User@list").with_name("user.list");
1186 assert_eq!(rule.name, Some("user.list".to_string()));
1187 }
1188
1189 #[test]
1194 fn test_route_group_new() {
1195 let g = RouteGroup::new("/api/v1");
1196 assert_eq!(g.prefix, "/api/v1");
1197 assert!(g.routes.is_empty());
1198 assert!(g.middleware.is_empty());
1199 }
1200
1201 #[test]
1202 fn test_route_group_add_route() {
1203 let mut g = RouteGroup::new("/api");
1204 g.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1205 assert_eq!(g.routes.len(), 1);
1206 }
1207
1208 #[test]
1209 fn test_route_group_with_middleware() {
1210 let g = RouteGroup::new("/api")
1211 .with_middleware("auth")
1212 .with_middleware("log");
1213 assert_eq!(g.middleware, vec!["auth", "log"]);
1214 }
1215
1216 #[test]
1221 fn test_join_path_basic() {
1222 assert_eq!(join_path("/api", "/users"), "/api/users");
1223 assert_eq!(join_path("/api/", "/users"), "/api/users");
1224 assert_eq!(join_path("/api", "users"), "/api/users");
1225 assert_eq!(join_path("/api/", "users"), "/api/users");
1226 }
1227
1228 #[test]
1229 fn test_join_path_empty_prefix() {
1230 assert_eq!(join_path("", "/users"), "/users");
1231 assert_eq!(join_path("", "users"), "/users");
1232 }
1233
1234 #[test]
1235 fn test_join_path_empty_path() {
1236 assert_eq!(join_path("/api", ""), "/api");
1237 assert_eq!(join_path("/api/", ""), "/api");
1238 }
1239
1240 #[test]
1241 fn test_join_path_both_empty() {
1242 assert_eq!(join_path("", ""), "");
1243 }
1244
1245 #[test]
1246 fn test_route_config_flatten_no_groups() {
1247 let mut config = RouteConfig::new();
1248 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1249 config.add_route(RouteRule::new(HttpMethod::POST, "/users", "User@create"));
1250
1251 let flat = config.flatten();
1252 assert_eq!(flat.len(), 2);
1253 assert_eq!(flat[0].path, "/users");
1254 assert_eq!(flat[1].path, "/users");
1255 }
1256
1257 #[test]
1258 fn test_route_config_flatten_with_group() {
1259 let mut config = RouteConfig::new();
1260 let mut group = RouteGroup::new("/api/v1");
1261 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1262 group.add_route(RouteRule::new(HttpMethod::POST, "/items", "Item@create"));
1263 config.add_group(group);
1264
1265 let flat = config.flatten();
1266 assert_eq!(flat.len(), 2);
1267 assert_eq!(flat[0].path, "/api/v1/items");
1268 assert_eq!(flat[1].path, "/api/v1/items");
1269 }
1270
1271 #[test]
1272 fn test_route_config_flatten_group_middleware_prepended() {
1273 let mut config = RouteConfig::new();
1274 let mut group = RouteGroup::new("/api");
1275 group.middleware = vec!["auth".to_string(), "log".to_string()];
1276 let mut rule = RouteRule::new(HttpMethod::GET, "/items", "Item@list");
1277 rule.middleware = vec!["cache".to_string()];
1278 group.routes.push(rule);
1279 config.add_group(group);
1280
1281 let flat = config.flatten();
1282 assert_eq!(flat[0].middleware, vec!["auth", "log", "cache"]);
1283 }
1284
1285 #[test]
1286 fn test_route_config_flatten_mixed() {
1287 let mut config = RouteConfig::new();
1288 config.add_route(RouteRule::new(HttpMethod::GET, "/health", "Health@check"));
1289 let mut group = RouteGroup::new("/api");
1290 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1291 config.add_group(group);
1292
1293 let flat = config.flatten();
1294 assert_eq!(flat.len(), 2);
1295 assert!(flat.iter().any(|r| r.path == "/health"));
1296 assert!(flat.iter().any(|r| r.path == "/api/items"));
1297 }
1298
1299 #[test]
1304 fn test_route_config_no_conflicts() {
1305 let mut config = RouteConfig::new();
1306 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1307 config.add_route(RouteRule::new(HttpMethod::POST, "/users", "User@create"));
1308 assert!(config.find_conflicts().is_empty());
1309 }
1310
1311 #[test]
1312 fn test_route_config_conflict_same_method_path() {
1313 let mut config = RouteConfig::new();
1314 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1315 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@all"));
1316
1317 let conflicts = config.find_conflicts();
1318 assert_eq!(conflicts.len(), 1);
1319 let (a, b) = &conflicts[0];
1320 assert_eq!(a.handler, "User@list");
1321 assert_eq!(b.handler, "User@all");
1322 }
1323
1324 #[test]
1325 fn test_route_config_no_conflict_different_method() {
1326 let mut config = RouteConfig::new();
1327 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1328 config.add_route(RouteRule::new(HttpMethod::DELETE, "/users", "User@delete"));
1329 assert!(config.find_conflicts().is_empty());
1330 }
1331
1332 #[test]
1333 fn test_route_config_conflict_in_group() {
1334 let mut config = RouteConfig::new();
1335 let mut group = RouteGroup::new("/api");
1336 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1337 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@all"));
1338 config.add_group(group);
1339
1340 let conflicts = config.find_conflicts();
1341 assert_eq!(conflicts.len(), 1);
1342 }
1343
1344 #[test]
1345 fn test_route_config_conflict_between_top_and_group() {
1346 let mut config = RouteConfig::new();
1347 config.add_route(RouteRule::new(HttpMethod::GET, "/api/items", "Item@list"));
1349 let mut group = RouteGroup::new("/api");
1351 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@all"));
1352 config.add_group(group);
1353
1354 let conflicts = config.find_conflicts();
1355 assert_eq!(conflicts.len(), 1);
1356 }
1357
1358 #[test]
1363 fn test_load_routes_from_yaml_str_simple() {
1364 let yaml = r#"
1365routes:
1366 - method: GET
1367 path: /users
1368 handler: User@list
1369 - method: POST
1370 path: /users
1371 handler: User@create
1372"#;
1373 let config = load_routes_from_yaml_str(yaml).unwrap();
1374 assert_eq!(config.routes.len(), 2);
1375 assert_eq!(config.routes[0].method, HttpMethod::GET);
1376 assert_eq!(config.routes[0].path, "/users");
1377 assert_eq!(config.routes[0].handler, "User@list");
1378 assert_eq!(config.routes[1].method, HttpMethod::POST);
1379 }
1380
1381 #[test]
1382 fn test_load_routes_from_yaml_str_with_groups() {
1383 let yaml = r#"
1384routes:
1385 - method: GET
1386 path: /health
1387 handler: Health@check
1388groups:
1389 - prefix: /api/v1
1390 middleware: [auth, log]
1391 routes:
1392 - method: GET
1393 path: /items
1394 handler: Item@list
1395 - method: POST
1396 path: /items
1397 handler: Item@create
1398"#;
1399 let config = load_routes_from_yaml_str(yaml).unwrap();
1400 assert_eq!(config.routes.len(), 1);
1401 assert_eq!(config.groups.len(), 1);
1402 assert_eq!(config.groups[0].prefix, "/api/v1");
1403 assert_eq!(config.groups[0].middleware, vec!["auth", "log"]);
1404 assert_eq!(config.groups[0].routes.len(), 2);
1405
1406 let flat = config.flatten();
1407 assert_eq!(flat.len(), 3);
1408 assert!(flat.iter().any(|r| r.path == "/health"));
1409 assert!(flat.iter().any(|r| r.path == "/api/v1/items"));
1410 }
1411
1412 #[test]
1413 fn test_load_routes_from_yaml_str_with_name_and_middleware() {
1414 let yaml = r#"
1415routes:
1416 - method: GET
1417 path: /users/{id}
1418 handler: User@show
1419 middleware: [auth, cache]
1420 name: user.show
1421"#;
1422 let config = load_routes_from_yaml_str(yaml).unwrap();
1423 assert_eq!(config.routes.len(), 1);
1424 let rule = &config.routes[0];
1425 assert_eq!(rule.middleware, vec!["auth", "cache"]);
1426 assert_eq!(rule.name, Some("user.show".to_string()));
1427 }
1428
1429 #[test]
1430 fn test_load_routes_from_yaml_str_empty() {
1431 let yaml = "";
1432 let config = load_routes_from_yaml_str(yaml).unwrap();
1433 assert_eq!(config.routes.len(), 0);
1434 assert_eq!(config.groups.len(), 0);
1435 }
1436
1437 #[test]
1438 fn test_load_routes_from_yaml_str_invalid_method() {
1439 let yaml = r#"
1440routes:
1441 - method: INVALID
1442 path: /users
1443 handler: User@list
1444"#;
1445 let result = load_routes_from_yaml_str(yaml);
1446 assert!(result.is_err());
1448 }
1449
1450 #[test]
1451 fn test_load_routes_from_yaml_str_invalid_yaml() {
1452 let yaml = "not: valid: yaml: at: all";
1453 let result = load_routes_from_yaml_str(yaml);
1454 assert!(result.is_err());
1455 }
1456
1457 #[test]
1462 fn test_load_routes_from_json_str_simple() {
1463 let json = r#"{
1464 "routes": [
1465 {"method": "GET", "path": "/users", "handler": "User@list"},
1466 {"method": "POST", "path": "/users", "handler": "User@create"}
1467 ]
1468}"#;
1469 let config = load_routes_from_json_str(json).unwrap();
1470 assert_eq!(config.routes.len(), 2);
1471 assert_eq!(config.routes[0].method, HttpMethod::GET);
1472 assert_eq!(config.routes[1].method, HttpMethod::POST);
1473 }
1474
1475 #[test]
1476 fn test_load_routes_from_json_str_with_groups() {
1477 let json = r#"{
1478 "routes": [
1479 {"method": "GET", "path": "/health", "handler": "Health@check"}
1480 ],
1481 "groups": [
1482 {
1483 "prefix": "/api",
1484 "middleware": ["auth"],
1485 "routes": [
1486 {"method": "GET", "path": "/items", "handler": "Item@list"}
1487 ]
1488 }
1489 ]
1490}"#;
1491 let config = load_routes_from_json_str(json).unwrap();
1492 assert_eq!(config.routes.len(), 1);
1493 assert_eq!(config.groups.len(), 1);
1494 assert_eq!(config.groups[0].prefix, "/api");
1495 }
1496
1497 #[test]
1498 fn test_load_routes_from_json_str_empty() {
1499 let json = "{}";
1500 let config = load_routes_from_json_str(json).unwrap();
1501 assert_eq!(config.routes.len(), 0);
1502 assert_eq!(config.groups.len(), 0);
1503 }
1504
1505 #[test]
1506 fn test_load_routes_from_json_str_invalid() {
1507 let json = "{not valid json";
1508 let result = load_routes_from_json_str(json);
1509 assert!(result.is_err());
1510 }
1511
1512 #[test]
1517 fn test_convention_route_from_uri_with_app() {
1518 let r = ConventionRoute::from_uri("/oapc/customer/index").unwrap();
1519 assert_eq!(r.app, "oapc");
1520 assert_eq!(r.controller, "Customer");
1521 assert_eq!(r.action, "index");
1522 assert_eq!(r.path, "/oapc/customer/index");
1523 assert_eq!(r.method, HttpMethod::GET);
1524 }
1525
1526 #[test]
1527 fn test_convention_route_from_uri_admin_app() {
1528 let r = ConventionRoute::from_uri("/admin/login/index").unwrap();
1529 assert_eq!(r.app, "admin");
1530 assert_eq!(r.controller, "Login");
1531 assert_eq!(r.action, "index");
1532 }
1533
1534 #[test]
1535 fn test_convention_route_from_uri_root_returns_none() {
1536 assert!(ConventionRoute::from_uri("/").is_none());
1538 assert!(ConventionRoute::from_uri("").is_none());
1539 }
1540
1541 #[test]
1542 fn test_convention_route_from_uri_single_segment() {
1543 let r = ConventionRoute::from_uri("/customer").unwrap();
1548 assert_eq!(r.app, "index");
1549 assert_eq!(r.controller, "Customer");
1550 assert_eq!(r.action, "index");
1551 }
1552
1553 #[test]
1554 fn test_convention_route_from_parsed() {
1555 let parsed = ParsedPath::new("api", "User", "list");
1556 let r = ConventionRoute::from_parsed(parsed).unwrap();
1557 assert_eq!(r.app, "api");
1558 assert_eq!(r.controller, "User");
1559 assert_eq!(r.action, "list");
1560 }
1561
1562 #[test]
1567 fn test_route_registry_new() {
1568 let r = RouteRegistry::new();
1569 assert!(r.attribute_routes.is_empty());
1570 assert!(r.config_routes.is_empty());
1571 assert!(r.convention_routes.is_empty());
1572 assert_eq!(r.total_count(), 0);
1573 }
1574
1575 #[test]
1576 fn test_route_registry_add_attribute_route() {
1577 let mut r = RouteRegistry::new();
1578 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1579 assert_eq!(r.attribute_routes.len(), 1);
1580 assert_eq!(r.total_count(), 1);
1581 }
1582
1583 #[test]
1584 fn test_route_registry_add_attribute_routes_batch() {
1585 let mut r = RouteRegistry::new();
1586 r.add_attribute_routes(vec![
1587 RouteRule::new(HttpMethod::GET, "/users", "User@list"),
1588 RouteRule::new(HttpMethod::POST, "/users", "User@create"),
1589 ]);
1590 assert_eq!(r.attribute_routes.len(), 2);
1591 }
1592
1593 #[test]
1594 fn test_route_registry_add_config_routes() {
1595 let mut r = RouteRegistry::new();
1596 let mut config = RouteConfig::new();
1597 config.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1598 config.add_route(RouteRule::new(HttpMethod::POST, "/items", "Item@create"));
1599 r.add_config_routes(&config);
1600 assert_eq!(r.config_routes.len(), 2);
1601 }
1602
1603 #[test]
1604 fn test_route_registry_add_convention_route() {
1605 let mut r = RouteRegistry::new();
1606 let cr = ConventionRoute::from_uri("/oapc/customer/index").unwrap();
1607 r.add_convention_route(cr);
1608 assert_eq!(r.convention_routes.len(), 1);
1609 }
1610
1611 #[test]
1612 fn test_route_registry_convention_as_rules() {
1613 let mut r = RouteRegistry::new();
1614 r.add_convention_route(ConventionRoute::from_uri("/oapc/customer/index").unwrap());
1615 r.add_convention_route(ConventionRoute::from_uri("/admin/login/index").unwrap());
1616
1617 let rules = r.convention_as_rules();
1618 assert_eq!(rules.len(), 2);
1619 assert_eq!(rules[0].handler, "Customer@index");
1620 assert_eq!(rules[1].handler, "Login@index");
1621 assert_eq!(
1622 rules[0].name,
1623 Some("convention.oapc.Customer.index".to_string())
1624 );
1625 }
1626
1627 #[test]
1628 fn test_route_registry_merged_rules_attribute_overrides_config() {
1629 let mut r = RouteRegistry::new();
1630 r.add_config_routes(&RouteConfig {
1632 routes: vec![RouteRule::new(HttpMethod::GET, "/users", "User@old")],
1633 groups: vec![],
1634 });
1635 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@new"));
1637
1638 let merged = r.merged_rules();
1639 assert_eq!(merged.len(), 1);
1640 assert_eq!(merged[0].handler, "User@new");
1641 }
1642
1643 #[test]
1644 fn test_route_registry_merged_rules_config_overrides_convention() {
1645 let mut r = RouteRegistry::new();
1646 r.add_convention_route(ConventionRoute::from_uri("/oapc/customer/index").unwrap());
1648 r.add_config_routes(&RouteConfig {
1650 routes: vec![RouteRule::new(
1651 HttpMethod::GET,
1652 "/oapc/customer/index",
1653 "Customer@custom",
1654 )],
1655 groups: vec![],
1656 });
1657
1658 let merged = r.merged_rules();
1659 assert_eq!(merged.len(), 1);
1660 assert_eq!(merged[0].handler, "Customer@custom");
1661 }
1662
1663 #[test]
1664 fn test_route_registry_merged_rules_different_paths_no_override() {
1665 let mut r = RouteRegistry::new();
1666 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1667 r.add_config_routes(&RouteConfig {
1668 routes: vec![RouteRule::new(HttpMethod::GET, "/items", "Item@list")],
1669 groups: vec![],
1670 });
1671 r.add_convention_route(ConventionRoute::from_uri("/oapc/customer/index").unwrap());
1672
1673 let merged = r.merged_rules();
1674 assert_eq!(merged.len(), 3);
1675 }
1676
1677 #[test]
1678 fn test_route_registry_attribute_conflicts() {
1679 let mut r = RouteRegistry::new();
1680 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1681 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@all"));
1682
1683 let conflicts = r.attribute_conflicts();
1684 assert_eq!(conflicts.len(), 1);
1685 }
1686
1687 #[test]
1688 fn test_route_registry_config_conflicts() {
1689 let mut r = RouteRegistry::new();
1690 r.add_config_routes(&RouteConfig {
1691 routes: vec![
1692 RouteRule::new(HttpMethod::GET, "/users", "User@list"),
1693 RouteRule::new(HttpMethod::GET, "/users", "User@all"),
1694 ],
1695 groups: vec![],
1696 });
1697
1698 let conflicts = r.config_conflicts();
1699 assert_eq!(conflicts.len(), 1);
1700 }
1701
1702 #[test]
1703 fn test_route_registry_no_conflicts() {
1704 let mut r = RouteRegistry::new();
1705 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1706 r.add_attribute_route(RouteRule::new(HttpMethod::POST, "/users", "User@create"));
1707
1708 assert!(r.attribute_conflicts().is_empty());
1709 }
1710
1711 #[test]
1712 fn test_route_registry_total_count() {
1713 let mut r = RouteRegistry::new();
1714 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/a", "A@index"));
1715 r.add_config_routes(&RouteConfig {
1716 routes: vec![RouteRule::new(HttpMethod::GET, "/b", "B@index")],
1717 groups: vec![],
1718 });
1719 r.add_convention_route(ConventionRoute::from_uri("/oapc/c/d").unwrap());
1720
1721 assert_eq!(r.total_count(), 3);
1722 }
1723
1724 #[test]
1729 fn test_integration_three_layer_routing() {
1730 let mut r = RouteRegistry::new();
1732 r.add_attribute_routes(vec![
1733 RouteRule::new(HttpMethod::GET, "/users", "User@list"),
1734 RouteRule::new(HttpMethod::POST, "/users", "User@create"),
1735 RouteRule::new(HttpMethod::GET, "/users/{id}", "User@show"),
1736 ]);
1737
1738 let yaml = r#"
1740routes:
1741 - method: GET
1742 path: /items
1743 handler: Item@list
1744 - method: POST
1745 path: /items
1746 handler: Item@create
1747groups:
1748 - prefix: /api/v1
1749 middleware: [auth]
1750 routes:
1751 - method: GET
1752 path: /orders
1753 handler: Order@list
1754"#;
1755 let config = load_routes_from_yaml_str(yaml).unwrap();
1756 r.add_config_routes(&config);
1757
1758 r.add_convention_route(ConventionRoute::from_uri("/oapc/customer/index").unwrap());
1760 r.add_convention_route(ConventionRoute::from_uri("/admin/login/index").unwrap());
1761
1762 assert_eq!(r.attribute_routes.len(), 3);
1764 assert_eq!(r.config_routes.len(), 3); assert_eq!(r.convention_routes.len(), 2);
1766 assert_eq!(r.total_count(), 8);
1767
1768 let merged = r.merged_rules();
1770 assert_eq!(merged.len(), 8);
1771
1772 assert!(r.attribute_conflicts().is_empty());
1774 assert!(r.config_conflicts().is_empty());
1775 }
1776
1777 #[test]
1778 fn test_integration_layer_override_priority() {
1779 let mut r = RouteRegistry::new();
1781
1782 r.add_convention_route(ConventionRoute {
1784 app: "index".to_string(),
1785 controller: "User".to_string(),
1786 action: "list".to_string(),
1787 method: HttpMethod::GET,
1788 path: "/users".to_string(),
1789 });
1790
1791 r.add_config_routes(&RouteConfig {
1793 routes: vec![RouteRule::new(HttpMethod::GET, "/users", "User@config")],
1794 groups: vec![],
1795 });
1796
1797 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@attribute"));
1799
1800 let merged = r.merged_rules();
1801 assert_eq!(merged.len(), 1);
1802 assert_eq!(merged[0].handler, "User@attribute");
1803 }
1804}