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, thiserror::Error)]
234pub enum RouteConfigError {
235 #[error("YAML parse error: {0}")]
237 YamlParse(#[from] serde_yml::Error),
238
239 #[error("JSON parse error: {0}")]
241 JsonParse(#[from] serde_json::Error),
242
243 #[error("invalid HTTP method: {0}")]
245 InvalidMethod(String),
246
247 #[error("empty handler string")]
249 EmptyHandler,
250
251 #[error("empty controller name in handler")]
253 EmptyController,
254
255 #[error("empty action name in handler")]
257 EmptyAction,
258
259 #[error("invalid controller name: {0}")]
261 InvalidController(String),
262
263 #[error("invalid action name: {0}")]
265 InvalidAction(String),
266
267 #[error("handler parse error: {0}")]
269 HandlerParse(String),
270
271 #[error("route conflict: {method} {path} already registered")]
273 Conflict {
274 method: String,
276 path: String,
278 },
279
280 #[error("failed to read route config file: {0}")]
282 FileRead(#[source] std::io::Error),
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289pub struct RouteRule {
290 pub method: HttpMethod,
292 pub path: String,
294 pub handler: String,
296 #[serde(default)]
298 pub middleware: Vec<String>,
299 #[serde(default)]
301 pub name: Option<String>,
302}
303
304impl RouteRule {
305 pub fn new(method: HttpMethod, path: impl Into<String>, handler: impl Into<String>) -> Self {
307 Self {
308 method,
309 path: path.into(),
310 handler: handler.into(),
311 middleware: Vec::new(),
312 name: None,
313 }
314 }
315
316 pub fn handler_ref(&self) -> Result<HandlerRef, RouteConfigError> {
318 HandlerRef::parse(&self.handler)
319 }
320
321 pub fn with_middleware(mut self, name: impl Into<String>) -> Self {
323 self.middleware.push(name.into());
324 self
325 }
326
327 pub fn with_name(mut self, name: impl Into<String>) -> Self {
329 self.name = Some(name.into());
330 self
331 }
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
338pub struct RouteConfig {
339 #[serde(default)]
341 pub routes: Vec<RouteRule>,
342 #[serde(default)]
344 pub groups: Vec<RouteGroup>,
345}
346
347impl RouteConfig {
348 pub fn new() -> Self {
350 Self::default()
351 }
352
353 pub fn add_route(&mut self, rule: RouteRule) {
355 self.routes.push(rule);
356 }
357
358 pub fn add_group(&mut self, group: RouteGroup) {
360 self.groups.push(group);
361 }
362
363 pub fn flatten(&self) -> Vec<RouteRule> {
368 let mut result = self.routes.clone();
369 for group in &self.groups {
370 for rule in &group.routes {
371 let mut flattened = rule.clone();
372 flattened.path = join_path(&group.prefix, &flattened.path);
373 let mut mw = group.middleware.clone();
375 mw.extend(flattened.middleware);
376 flattened.middleware = mw;
377 result.push(flattened);
378 }
379 }
380 result
381 }
382
383 pub fn find_conflicts(&self) -> Vec<(RouteRule, RouteRule)> {
387 let flattened = self.flatten();
388 let mut seen: HashMap<(String, String), usize> = HashMap::new();
389 let mut conflicts = Vec::new();
390
391 for (i, rule) in flattened.iter().enumerate() {
392 let key = (rule.method.to_string(), rule.path.clone());
393 if let Some(&prev_idx) = seen.get(&key) {
394 conflicts.push((flattened[prev_idx].clone(), flattened[i].clone()));
395 } else {
396 seen.insert(key, i);
397 }
398 }
399
400 conflicts
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
408pub struct RouteGroup {
409 pub prefix: String,
411 #[serde(default)]
413 pub routes: Vec<RouteRule>,
414 #[serde(default)]
416 pub middleware: Vec<String>,
417}
418
419impl RouteGroup {
420 pub fn new(prefix: impl Into<String>) -> Self {
422 Self {
423 prefix: prefix.into(),
424 routes: Vec::new(),
425 middleware: Vec::new(),
426 }
427 }
428
429 pub fn add_route(&mut self, rule: RouteRule) -> &mut Self {
431 self.routes.push(rule);
432 self
433 }
434
435 pub fn with_middleware(mut self, name: impl Into<String>) -> Self {
437 self.middleware.push(name.into());
438 self
439 }
440}
441
442fn join_path(prefix: &str, path: &str) -> String {
444 let prefix = prefix.trim_end_matches('/');
445 let path = path.trim_start_matches('/');
446 if path.is_empty() {
447 prefix.to_string()
448 } else if prefix.is_empty() {
449 format!("/{path}")
450 } else {
451 format!("{prefix}/{path}")
452 }
453}
454
455#[tracing::instrument]
480pub fn load_routes_from_yaml_str(yaml: &str) -> Result<RouteConfig, RouteConfigError> {
481 let config: RouteConfig = serde_yml::from_str(yaml)?;
482 Ok(config)
483}
484
485#[tracing::instrument]
487pub fn load_routes_from_json_str(json: &str) -> Result<RouteConfig, RouteConfigError> {
488 let config: RouteConfig = serde_json::from_str(json)?;
489 Ok(config)
490}
491
492#[tracing::instrument(skip(path))]
494pub async fn load_routes_from_yaml_file(
495 path: impl AsRef<std::path::Path>,
496) -> Result<RouteConfig, RouteConfigError> {
497 let content = tokio::fs::read_to_string(path)
498 .await
499 .map_err(RouteConfigError::FileRead)?;
500 load_routes_from_yaml_str(&content)
501}
502
503#[tracing::instrument(skip(path))]
505pub async fn load_routes_from_json_file(
506 path: impl AsRef<std::path::Path>,
507) -> Result<RouteConfig, RouteConfigError> {
508 let content = tokio::fs::read_to_string(path)
509 .await
510 .map_err(RouteConfigError::FileRead)?;
511 load_routes_from_json_str(&content)
512}
513
514pub trait ControllerRouter {
539 fn router_rules(&self) -> Vec<RouteRule>;
541
542 fn router_prefix(&self) -> &str {
544 ""
545 }
546
547 fn router_middleware(&self) -> Vec<String> {
549 Vec::new()
550 }
551}
552
553#[derive(Debug, Clone, PartialEq, Eq)]
562pub struct ConventionRoute {
563 pub app: String,
565 pub controller: String,
567 pub action: String,
569 pub method: HttpMethod,
571 pub path: String,
573}
574
575impl ConventionRoute {
576 pub fn from_uri(uri: &str) -> Option<Self> {
588 let parsed = crate::router::parse_path(uri);
589 if parsed.app == crate::router::DEFAULT_APP
591 && parsed.controller == crate::router::DEFAULT_CONTROLLER
592 && parsed.action == crate::router::DEFAULT_ACTION
593 {
594 return None;
595 }
596 let path = format!(
597 "/{}/{}/{}",
598 parsed.app,
599 parsed.controller.to_lowercase(),
600 parsed.action
601 );
602 Some(Self {
603 app: parsed.app,
604 controller: parsed.controller,
605 action: parsed.action,
606 method: HttpMethod::GET,
607 path,
608 })
609 }
610
611 pub fn from_parsed(parsed: ParsedPath) -> Option<Self> {
613 let uri = format!(
614 "/{}/{}/{}",
615 parsed.app,
616 parsed.controller.to_lowercase(),
617 parsed.action
618 );
619 Self::from_uri(&uri)
620 }
621}
622
623#[derive(Debug, Clone, Default)]
636pub struct RouteRegistry {
637 pub attribute_routes: Vec<RouteRule>,
639 pub config_routes: Vec<RouteRule>,
641 pub convention_routes: Vec<ConventionRoute>,
643}
644
645impl RouteRegistry {
646 pub fn new() -> Self {
648 Self::default()
649 }
650
651 pub fn add_attribute_route(&mut self, rule: RouteRule) -> &mut Self {
653 self.attribute_routes.push(rule);
654 self
655 }
656
657 pub fn add_attribute_routes(
659 &mut self,
660 rules: impl IntoIterator<Item = RouteRule>,
661 ) -> &mut Self {
662 self.attribute_routes.extend(rules);
663 self
664 }
665
666 pub fn add_config_routes(&mut self, config: &RouteConfig) -> &mut Self {
668 self.config_routes.extend(config.flatten());
669 self
670 }
671
672 pub fn add_convention_route(&mut self, route: ConventionRoute) -> &mut Self {
674 self.convention_routes.push(route);
675 self
676 }
677
678 #[tracing::instrument(skip(self))]
682 pub fn convention_as_rules(&self) -> Vec<RouteRule> {
683 self.convention_routes
684 .iter()
685 .map(|c| RouteRule {
686 method: c.method.clone(),
687 path: c.path.clone(),
688 handler: format!("{}@{}", c.controller, c.action),
689 middleware: Vec::new(),
690 name: Some(format!(
691 "convention.{}.{}.{}",
692 c.app, c.controller, c.action
693 )),
694 })
695 .collect()
696 }
697
698 #[tracing::instrument(skip(self))]
702 pub fn merged_rules(&self) -> Vec<RouteRule> {
703 let mut seen: HashMap<(String, String), RouteRule> = HashMap::new();
704
705 for rule in self.convention_as_rules() {
707 let key = (rule.method.to_string(), rule.path.clone());
708 seen.insert(key, rule);
709 }
710 for rule in &self.config_routes {
711 let key = (rule.method.to_string(), rule.path.clone());
712 seen.insert(key, rule.clone());
713 }
714 for rule in &self.attribute_routes {
715 let key = (rule.method.to_string(), rule.path.clone());
716 seen.insert(key, rule.clone());
717 }
718
719 seen.into_values().collect()
720 }
721
722 pub fn attribute_conflicts(&self) -> Vec<(RouteRule, RouteRule)> {
724 find_conflicts_in(&self.attribute_routes)
725 }
726
727 pub fn config_conflicts(&self) -> Vec<(RouteRule, RouteRule)> {
729 find_conflicts_in(&self.config_routes)
730 }
731
732 pub fn total_count(&self) -> usize {
734 self.attribute_routes.len() + self.config_routes.len() + self.convention_routes.len()
735 }
736}
737
738fn find_conflicts_in(rules: &[RouteRule]) -> Vec<(RouteRule, RouteRule)> {
740 let mut seen: HashMap<(String, String), usize> = HashMap::new();
741 let mut conflicts = Vec::new();
742
743 for (i, rule) in rules.iter().enumerate() {
744 let key = (rule.method.to_string(), rule.path.clone());
745 if let Some(&prev_idx) = seen.get(&key) {
746 conflicts.push((rules[prev_idx].clone(), rules[i].clone()));
747 } else {
748 seen.insert(key, i);
749 }
750 }
751
752 conflicts
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[test]
764 fn test_http_method_parse_uppercase() {
765 assert_eq!(HttpMethod::parse("GET").unwrap(), HttpMethod::GET);
766 assert_eq!(HttpMethod::parse("POST").unwrap(), HttpMethod::POST);
767 assert_eq!(HttpMethod::parse("PUT").unwrap(), HttpMethod::PUT);
768 assert_eq!(HttpMethod::parse("DELETE").unwrap(), HttpMethod::DELETE);
769 assert_eq!(HttpMethod::parse("PATCH").unwrap(), HttpMethod::PATCH);
770 assert_eq!(HttpMethod::parse("OPTIONS").unwrap(), HttpMethod::OPTIONS);
771 }
772
773 #[test]
774 fn test_http_method_parse_lowercase() {
775 assert_eq!(HttpMethod::parse("get").unwrap(), HttpMethod::GET);
776 assert_eq!(HttpMethod::parse("post").unwrap(), HttpMethod::POST);
777 }
778
779 #[test]
780 fn test_http_method_parse_mixed_case() {
781 assert_eq!(HttpMethod::parse("Get").unwrap(), HttpMethod::GET);
782 assert_eq!(HttpMethod::parse("pOsT").unwrap(), HttpMethod::POST);
783 }
784
785 #[test]
786 fn test_http_method_parse_invalid() {
787 assert!(HttpMethod::parse("invalid").is_err());
788 assert!(HttpMethod::parse("").is_err());
789 assert!(HttpMethod::parse("CONNECT").is_err());
790 assert!(HttpMethod::parse("TRACE").is_err());
791 }
792
793 #[test]
794 fn test_http_method_to_axum() {
795 assert_eq!(HttpMethod::GET.to_axum_method(), axum::http::Method::GET);
796 assert_eq!(HttpMethod::POST.to_axum_method(), axum::http::Method::POST);
797 assert_eq!(HttpMethod::PUT.to_axum_method(), axum::http::Method::PUT);
798 assert_eq!(
799 HttpMethod::DELETE.to_axum_method(),
800 axum::http::Method::DELETE
801 );
802 assert_eq!(
803 HttpMethod::PATCH.to_axum_method(),
804 axum::http::Method::PATCH
805 );
806 assert_eq!(
807 HttpMethod::OPTIONS.to_axum_method(),
808 axum::http::Method::OPTIONS
809 );
810 }
811
812 #[test]
813 fn test_http_method_display() {
814 assert_eq!(HttpMethod::GET.to_string(), "GET");
815 assert_eq!(HttpMethod::POST.to_string(), "POST");
816 assert_eq!(HttpMethod::PUT.to_string(), "PUT");
817 }
818
819 #[test]
820 fn test_http_method_serde() {
821 let json = serde_json::to_string(&HttpMethod::GET).unwrap();
822 assert_eq!(json, "\"GET\"");
823
824 let m: HttpMethod = serde_json::from_str("\"POST\"").unwrap();
825 assert_eq!(m, HttpMethod::POST);
826 }
827
828 #[test]
833 fn test_handler_ref_parse_at_separator() {
834 let h = HandlerRef::parse("User@list").unwrap();
835 assert_eq!(h.controller, "User");
836 assert_eq!(h.action, "list");
837 }
838
839 #[test]
840 fn test_handler_ref_parse_slash_separator() {
841 let h = HandlerRef::parse("User/list").unwrap();
842 assert_eq!(h.controller, "User");
843 assert_eq!(h.action, "list");
844 }
845
846 #[test]
847 fn test_handler_ref_parse_only_controller() {
848 let h = HandlerRef::parse("User").unwrap();
849 assert_eq!(h.controller, "User");
850 assert_eq!(h.action, "index"); }
852
853 #[test]
854 fn test_handler_ref_parse_with_whitespace() {
855 let h = HandlerRef::parse(" User @ list ").unwrap();
856 assert_eq!(h.controller, "User");
857 assert_eq!(h.action, "list");
858 }
859
860 #[test]
861 fn test_handler_ref_parse_empty() {
862 assert!(HandlerRef::parse("").is_err());
863 assert!(HandlerRef::parse(" ").is_err());
864 }
865
866 #[test]
867 fn test_handler_ref_parse_empty_controller() {
868 assert!(HandlerRef::parse("@list").is_err());
869 assert!(HandlerRef::parse("/list").is_err());
870 }
871
872 #[test]
873 fn test_handler_ref_parse_empty_action() {
874 assert!(HandlerRef::parse("User@").is_err());
875 assert!(HandlerRef::parse("User/").is_err());
876 }
877
878 #[test]
879 fn test_handler_ref_to_string() {
880 let h = HandlerRef {
881 controller: "User".to_string(),
882 action: "list".to_string(),
883 };
884 assert_eq!(h.to_string(), "User@list");
885 }
886
887 #[test]
892 fn test_handler_ref_parse_rejects_path_traversal() {
893 assert!(matches!(
895 HandlerRef::parse("../Secret@admin"),
896 Err(RouteConfigError::InvalidController(_))
897 ));
898 assert!(matches!(
900 HandlerRef::parse("..@admin"),
901 Err(RouteConfigError::InvalidController(_))
902 ));
903 assert!(matches!(
905 HandlerRef::parse("User@../evil"),
906 Err(RouteConfigError::InvalidAction(_))
907 ));
908 }
909
910 #[test]
911 fn test_handler_ref_parse_rejects_double_at() {
912 assert!(matches!(
915 HandlerRef::parse("User@list@extra"),
916 Err(RouteConfigError::InvalidAction(_))
917 ));
918 }
919
920 #[test]
921 fn test_handler_ref_parse_rejects_space_injection() {
922 assert!(matches!(
924 HandlerRef::parse("Us er@list"),
925 Err(RouteConfigError::InvalidController(_))
926 ));
927 assert!(matches!(
928 HandlerRef::parse("User@li st"),
929 Err(RouteConfigError::InvalidAction(_))
930 ));
931 }
932
933 #[test]
934 fn test_handler_ref_parse_rejects_leading_digit() {
935 assert!(matches!(
937 HandlerRef::parse("1User@list"),
938 Err(RouteConfigError::InvalidController(_))
939 ));
940 assert!(matches!(
941 HandlerRef::parse("User@1list"),
942 Err(RouteConfigError::InvalidAction(_))
943 ));
944 }
945
946 #[test]
947 fn test_handler_ref_parse_accepts_underscore_and_alphanumeric() {
948 let h = HandlerRef::parse("_Private@_index").unwrap();
949 assert_eq!(h.controller, "_Private");
950 assert_eq!(h.action, "_index");
951
952 let h = HandlerRef::parse("User@action_1").unwrap();
953 assert_eq!(h.controller, "User");
954 assert_eq!(h.action, "action_1");
955
956 let h = HandlerRef::parse("CustomerList@getListById").unwrap();
958 assert_eq!(h.controller, "CustomerList");
959 assert_eq!(h.action, "getListById");
960 }
961
962 #[test]
963 fn test_handler_ref_parse_rejects_special_chars() {
964 assert!(HandlerRef::parse("User:list@action").is_err());
966 assert!(HandlerRef::parse("User;list@action").is_err());
967 assert!(HandlerRef::parse(r"User\list@action").is_err());
968 assert!(HandlerRef::parse("User@act\nion").is_err());
969 }
970
971 #[test]
976 fn test_route_rule_new() {
977 let rule = RouteRule::new(HttpMethod::GET, "/users", "User@list");
978 assert_eq!(rule.method, HttpMethod::GET);
979 assert_eq!(rule.path, "/users");
980 assert_eq!(rule.handler, "User@list");
981 assert!(rule.middleware.is_empty());
982 assert!(rule.name.is_none());
983 }
984
985 #[test]
986 fn test_route_rule_handler_ref() {
987 let rule = RouteRule::new(HttpMethod::GET, "/users", "User@list");
988 let h = rule.handler_ref().unwrap();
989 assert_eq!(h.controller, "User");
990 assert_eq!(h.action, "list");
991 }
992
993 #[test]
994 fn test_route_rule_with_middleware() {
995 let rule = RouteRule::new(HttpMethod::GET, "/users", "User@list")
996 .with_middleware("auth")
997 .with_middleware("log");
998 assert_eq!(rule.middleware, vec!["auth", "log"]);
999 }
1000
1001 #[test]
1002 fn test_route_rule_with_name() {
1003 let rule = RouteRule::new(HttpMethod::GET, "/users", "User@list").with_name("user.list");
1004 assert_eq!(rule.name, Some("user.list".to_string()));
1005 }
1006
1007 #[test]
1012 fn test_route_group_new() {
1013 let g = RouteGroup::new("/api/v1");
1014 assert_eq!(g.prefix, "/api/v1");
1015 assert!(g.routes.is_empty());
1016 assert!(g.middleware.is_empty());
1017 }
1018
1019 #[test]
1020 fn test_route_group_add_route() {
1021 let mut g = RouteGroup::new("/api");
1022 g.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1023 assert_eq!(g.routes.len(), 1);
1024 }
1025
1026 #[test]
1027 fn test_route_group_with_middleware() {
1028 let g = RouteGroup::new("/api")
1029 .with_middleware("auth")
1030 .with_middleware("log");
1031 assert_eq!(g.middleware, vec!["auth", "log"]);
1032 }
1033
1034 #[test]
1039 fn test_join_path_basic() {
1040 assert_eq!(join_path("/api", "/users"), "/api/users");
1041 assert_eq!(join_path("/api/", "/users"), "/api/users");
1042 assert_eq!(join_path("/api", "users"), "/api/users");
1043 assert_eq!(join_path("/api/", "users"), "/api/users");
1044 }
1045
1046 #[test]
1047 fn test_join_path_empty_prefix() {
1048 assert_eq!(join_path("", "/users"), "/users");
1049 assert_eq!(join_path("", "users"), "/users");
1050 }
1051
1052 #[test]
1053 fn test_join_path_empty_path() {
1054 assert_eq!(join_path("/api", ""), "/api");
1055 assert_eq!(join_path("/api/", ""), "/api");
1056 }
1057
1058 #[test]
1059 fn test_join_path_both_empty() {
1060 assert_eq!(join_path("", ""), "");
1061 }
1062
1063 #[test]
1064 fn test_route_config_flatten_no_groups() {
1065 let mut config = RouteConfig::new();
1066 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1067 config.add_route(RouteRule::new(HttpMethod::POST, "/users", "User@create"));
1068
1069 let flat = config.flatten();
1070 assert_eq!(flat.len(), 2);
1071 assert_eq!(flat[0].path, "/users");
1072 assert_eq!(flat[1].path, "/users");
1073 }
1074
1075 #[test]
1076 fn test_route_config_flatten_with_group() {
1077 let mut config = RouteConfig::new();
1078 let mut group = RouteGroup::new("/api/v1");
1079 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1080 group.add_route(RouteRule::new(HttpMethod::POST, "/items", "Item@create"));
1081 config.add_group(group);
1082
1083 let flat = config.flatten();
1084 assert_eq!(flat.len(), 2);
1085 assert_eq!(flat[0].path, "/api/v1/items");
1086 assert_eq!(flat[1].path, "/api/v1/items");
1087 }
1088
1089 #[test]
1090 fn test_route_config_flatten_group_middleware_prepended() {
1091 let mut config = RouteConfig::new();
1092 let mut group = RouteGroup::new("/api");
1093 group.middleware = vec!["auth".to_string(), "log".to_string()];
1094 let mut rule = RouteRule::new(HttpMethod::GET, "/items", "Item@list");
1095 rule.middleware = vec!["cache".to_string()];
1096 group.routes.push(rule);
1097 config.add_group(group);
1098
1099 let flat = config.flatten();
1100 assert_eq!(flat[0].middleware, vec!["auth", "log", "cache"]);
1101 }
1102
1103 #[test]
1104 fn test_route_config_flatten_mixed() {
1105 let mut config = RouteConfig::new();
1106 config.add_route(RouteRule::new(HttpMethod::GET, "/health", "Health@check"));
1107 let mut group = RouteGroup::new("/api");
1108 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1109 config.add_group(group);
1110
1111 let flat = config.flatten();
1112 assert_eq!(flat.len(), 2);
1113 assert!(flat.iter().any(|r| r.path == "/health"));
1114 assert!(flat.iter().any(|r| r.path == "/api/items"));
1115 }
1116
1117 #[test]
1122 fn test_route_config_no_conflicts() {
1123 let mut config = RouteConfig::new();
1124 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1125 config.add_route(RouteRule::new(HttpMethod::POST, "/users", "User@create"));
1126 assert!(config.find_conflicts().is_empty());
1127 }
1128
1129 #[test]
1130 fn test_route_config_conflict_same_method_path() {
1131 let mut config = RouteConfig::new();
1132 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1133 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@all"));
1134
1135 let conflicts = config.find_conflicts();
1136 assert_eq!(conflicts.len(), 1);
1137 let (a, b) = &conflicts[0];
1138 assert_eq!(a.handler, "User@list");
1139 assert_eq!(b.handler, "User@all");
1140 }
1141
1142 #[test]
1143 fn test_route_config_no_conflict_different_method() {
1144 let mut config = RouteConfig::new();
1145 config.add_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1146 config.add_route(RouteRule::new(HttpMethod::DELETE, "/users", "User@delete"));
1147 assert!(config.find_conflicts().is_empty());
1148 }
1149
1150 #[test]
1151 fn test_route_config_conflict_in_group() {
1152 let mut config = RouteConfig::new();
1153 let mut group = RouteGroup::new("/api");
1154 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1155 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@all"));
1156 config.add_group(group);
1157
1158 let conflicts = config.find_conflicts();
1159 assert_eq!(conflicts.len(), 1);
1160 }
1161
1162 #[test]
1163 fn test_route_config_conflict_between_top_and_group() {
1164 let mut config = RouteConfig::new();
1165 config.add_route(RouteRule::new(HttpMethod::GET, "/api/items", "Item@list"));
1167 let mut group = RouteGroup::new("/api");
1169 group.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@all"));
1170 config.add_group(group);
1171
1172 let conflicts = config.find_conflicts();
1173 assert_eq!(conflicts.len(), 1);
1174 }
1175
1176 #[test]
1181 fn test_load_routes_from_yaml_str_simple() {
1182 let yaml = r#"
1183routes:
1184 - method: GET
1185 path: /users
1186 handler: User@list
1187 - method: POST
1188 path: /users
1189 handler: User@create
1190"#;
1191 let config = load_routes_from_yaml_str(yaml).unwrap();
1192 assert_eq!(config.routes.len(), 2);
1193 assert_eq!(config.routes[0].method, HttpMethod::GET);
1194 assert_eq!(config.routes[0].path, "/users");
1195 assert_eq!(config.routes[0].handler, "User@list");
1196 assert_eq!(config.routes[1].method, HttpMethod::POST);
1197 }
1198
1199 #[test]
1200 fn test_load_routes_from_yaml_str_with_groups() {
1201 let yaml = r#"
1202routes:
1203 - method: GET
1204 path: /health
1205 handler: Health@check
1206groups:
1207 - prefix: /api/v1
1208 middleware: [auth, log]
1209 routes:
1210 - method: GET
1211 path: /items
1212 handler: Item@list
1213 - method: POST
1214 path: /items
1215 handler: Item@create
1216"#;
1217 let config = load_routes_from_yaml_str(yaml).unwrap();
1218 assert_eq!(config.routes.len(), 1);
1219 assert_eq!(config.groups.len(), 1);
1220 assert_eq!(config.groups[0].prefix, "/api/v1");
1221 assert_eq!(config.groups[0].middleware, vec!["auth", "log"]);
1222 assert_eq!(config.groups[0].routes.len(), 2);
1223
1224 let flat = config.flatten();
1225 assert_eq!(flat.len(), 3);
1226 assert!(flat.iter().any(|r| r.path == "/health"));
1227 assert!(flat.iter().any(|r| r.path == "/api/v1/items"));
1228 }
1229
1230 #[test]
1231 fn test_load_routes_from_yaml_str_with_name_and_middleware() {
1232 let yaml = r#"
1233routes:
1234 - method: GET
1235 path: /users/{id}
1236 handler: User@show
1237 middleware: [auth, cache]
1238 name: user.show
1239"#;
1240 let config = load_routes_from_yaml_str(yaml).unwrap();
1241 assert_eq!(config.routes.len(), 1);
1242 let rule = &config.routes[0];
1243 assert_eq!(rule.middleware, vec!["auth", "cache"]);
1244 assert_eq!(rule.name, Some("user.show".to_string()));
1245 }
1246
1247 #[test]
1248 fn test_load_routes_from_yaml_str_empty() {
1249 let yaml = "";
1250 let config = load_routes_from_yaml_str(yaml).unwrap();
1251 assert_eq!(config.routes.len(), 0);
1252 assert_eq!(config.groups.len(), 0);
1253 }
1254
1255 #[test]
1256 fn test_load_routes_from_yaml_str_invalid_method() {
1257 let yaml = r#"
1258routes:
1259 - method: INVALID
1260 path: /users
1261 handler: User@list
1262"#;
1263 let result = load_routes_from_yaml_str(yaml);
1264 assert!(result.is_err());
1266 }
1267
1268 #[test]
1269 fn test_load_routes_from_yaml_str_invalid_yaml() {
1270 let yaml = "not: valid: yaml: at: all";
1271 let result = load_routes_from_yaml_str(yaml);
1272 assert!(result.is_err());
1273 }
1274
1275 #[test]
1280 fn test_load_routes_from_json_str_simple() {
1281 let json = r#"{
1282 "routes": [
1283 {"method": "GET", "path": "/users", "handler": "User@list"},
1284 {"method": "POST", "path": "/users", "handler": "User@create"}
1285 ]
1286}"#;
1287 let config = load_routes_from_json_str(json).unwrap();
1288 assert_eq!(config.routes.len(), 2);
1289 assert_eq!(config.routes[0].method, HttpMethod::GET);
1290 assert_eq!(config.routes[1].method, HttpMethod::POST);
1291 }
1292
1293 #[test]
1294 fn test_load_routes_from_json_str_with_groups() {
1295 let json = r#"{
1296 "routes": [
1297 {"method": "GET", "path": "/health", "handler": "Health@check"}
1298 ],
1299 "groups": [
1300 {
1301 "prefix": "/api",
1302 "middleware": ["auth"],
1303 "routes": [
1304 {"method": "GET", "path": "/items", "handler": "Item@list"}
1305 ]
1306 }
1307 ]
1308}"#;
1309 let config = load_routes_from_json_str(json).unwrap();
1310 assert_eq!(config.routes.len(), 1);
1311 assert_eq!(config.groups.len(), 1);
1312 assert_eq!(config.groups[0].prefix, "/api");
1313 }
1314
1315 #[test]
1316 fn test_load_routes_from_json_str_empty() {
1317 let json = "{}";
1318 let config = load_routes_from_json_str(json).unwrap();
1319 assert_eq!(config.routes.len(), 0);
1320 assert_eq!(config.groups.len(), 0);
1321 }
1322
1323 #[test]
1324 fn test_load_routes_from_json_str_invalid() {
1325 let json = "{not valid json";
1326 let result = load_routes_from_json_str(json);
1327 assert!(result.is_err());
1328 }
1329
1330 #[test]
1335 fn test_convention_route_from_uri_with_app() {
1336 let r = ConventionRoute::from_uri("/oapc/customer/index").unwrap();
1337 assert_eq!(r.app, "oapc");
1338 assert_eq!(r.controller, "Customer");
1339 assert_eq!(r.action, "index");
1340 assert_eq!(r.path, "/oapc/customer/index");
1341 assert_eq!(r.method, HttpMethod::GET);
1342 }
1343
1344 #[test]
1345 fn test_convention_route_from_uri_admin_app() {
1346 let r = ConventionRoute::from_uri("/admin/login/index").unwrap();
1347 assert_eq!(r.app, "admin");
1348 assert_eq!(r.controller, "Login");
1349 assert_eq!(r.action, "index");
1350 }
1351
1352 #[test]
1353 fn test_convention_route_from_uri_root_returns_none() {
1354 assert!(ConventionRoute::from_uri("/").is_none());
1356 assert!(ConventionRoute::from_uri("").is_none());
1357 }
1358
1359 #[test]
1360 fn test_convention_route_from_uri_single_segment() {
1361 let r = ConventionRoute::from_uri("/customer").unwrap();
1366 assert_eq!(r.app, "index");
1367 assert_eq!(r.controller, "Customer");
1368 assert_eq!(r.action, "index");
1369 }
1370
1371 #[test]
1372 fn test_convention_route_from_parsed() {
1373 let parsed = ParsedPath::new("api", "User", "list");
1374 let r = ConventionRoute::from_parsed(parsed).unwrap();
1375 assert_eq!(r.app, "api");
1376 assert_eq!(r.controller, "User");
1377 assert_eq!(r.action, "list");
1378 }
1379
1380 #[test]
1385 fn test_route_registry_new() {
1386 let r = RouteRegistry::new();
1387 assert!(r.attribute_routes.is_empty());
1388 assert!(r.config_routes.is_empty());
1389 assert!(r.convention_routes.is_empty());
1390 assert_eq!(r.total_count(), 0);
1391 }
1392
1393 #[test]
1394 fn test_route_registry_add_attribute_route() {
1395 let mut r = RouteRegistry::new();
1396 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1397 assert_eq!(r.attribute_routes.len(), 1);
1398 assert_eq!(r.total_count(), 1);
1399 }
1400
1401 #[test]
1402 fn test_route_registry_add_attribute_routes_batch() {
1403 let mut r = RouteRegistry::new();
1404 r.add_attribute_routes(vec![
1405 RouteRule::new(HttpMethod::GET, "/users", "User@list"),
1406 RouteRule::new(HttpMethod::POST, "/users", "User@create"),
1407 ]);
1408 assert_eq!(r.attribute_routes.len(), 2);
1409 }
1410
1411 #[test]
1412 fn test_route_registry_add_config_routes() {
1413 let mut r = RouteRegistry::new();
1414 let mut config = RouteConfig::new();
1415 config.add_route(RouteRule::new(HttpMethod::GET, "/items", "Item@list"));
1416 config.add_route(RouteRule::new(HttpMethod::POST, "/items", "Item@create"));
1417 r.add_config_routes(&config);
1418 assert_eq!(r.config_routes.len(), 2);
1419 }
1420
1421 #[test]
1422 fn test_route_registry_add_convention_route() {
1423 let mut r = RouteRegistry::new();
1424 let cr = ConventionRoute::from_uri("/oapc/customer/index").unwrap();
1425 r.add_convention_route(cr);
1426 assert_eq!(r.convention_routes.len(), 1);
1427 }
1428
1429 #[test]
1430 fn test_route_registry_convention_as_rules() {
1431 let mut r = RouteRegistry::new();
1432 r.add_convention_route(ConventionRoute::from_uri("/oapc/customer/index").unwrap());
1433 r.add_convention_route(ConventionRoute::from_uri("/admin/login/index").unwrap());
1434
1435 let rules = r.convention_as_rules();
1436 assert_eq!(rules.len(), 2);
1437 assert_eq!(rules[0].handler, "Customer@index");
1438 assert_eq!(rules[1].handler, "Login@index");
1439 assert_eq!(
1440 rules[0].name,
1441 Some("convention.oapc.Customer.index".to_string())
1442 );
1443 }
1444
1445 #[test]
1446 fn test_route_registry_merged_rules_attribute_overrides_config() {
1447 let mut r = RouteRegistry::new();
1448 r.add_config_routes(&RouteConfig {
1450 routes: vec![RouteRule::new(HttpMethod::GET, "/users", "User@old")],
1451 groups: vec![],
1452 });
1453 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@new"));
1455
1456 let merged = r.merged_rules();
1457 assert_eq!(merged.len(), 1);
1458 assert_eq!(merged[0].handler, "User@new");
1459 }
1460
1461 #[test]
1462 fn test_route_registry_merged_rules_config_overrides_convention() {
1463 let mut r = RouteRegistry::new();
1464 r.add_convention_route(ConventionRoute::from_uri("/oapc/customer/index").unwrap());
1466 r.add_config_routes(&RouteConfig {
1468 routes: vec![RouteRule::new(
1469 HttpMethod::GET,
1470 "/oapc/customer/index",
1471 "Customer@custom",
1472 )],
1473 groups: vec![],
1474 });
1475
1476 let merged = r.merged_rules();
1477 assert_eq!(merged.len(), 1);
1478 assert_eq!(merged[0].handler, "Customer@custom");
1479 }
1480
1481 #[test]
1482 fn test_route_registry_merged_rules_different_paths_no_override() {
1483 let mut r = RouteRegistry::new();
1484 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1485 r.add_config_routes(&RouteConfig {
1486 routes: vec![RouteRule::new(HttpMethod::GET, "/items", "Item@list")],
1487 groups: vec![],
1488 });
1489 r.add_convention_route(ConventionRoute::from_uri("/oapc/customer/index").unwrap());
1490
1491 let merged = r.merged_rules();
1492 assert_eq!(merged.len(), 3);
1493 }
1494
1495 #[test]
1496 fn test_route_registry_attribute_conflicts() {
1497 let mut r = RouteRegistry::new();
1498 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1499 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@all"));
1500
1501 let conflicts = r.attribute_conflicts();
1502 assert_eq!(conflicts.len(), 1);
1503 }
1504
1505 #[test]
1506 fn test_route_registry_config_conflicts() {
1507 let mut r = RouteRegistry::new();
1508 r.add_config_routes(&RouteConfig {
1509 routes: vec![
1510 RouteRule::new(HttpMethod::GET, "/users", "User@list"),
1511 RouteRule::new(HttpMethod::GET, "/users", "User@all"),
1512 ],
1513 groups: vec![],
1514 });
1515
1516 let conflicts = r.config_conflicts();
1517 assert_eq!(conflicts.len(), 1);
1518 }
1519
1520 #[test]
1521 fn test_route_registry_no_conflicts() {
1522 let mut r = RouteRegistry::new();
1523 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@list"));
1524 r.add_attribute_route(RouteRule::new(HttpMethod::POST, "/users", "User@create"));
1525
1526 assert!(r.attribute_conflicts().is_empty());
1527 }
1528
1529 #[test]
1530 fn test_route_registry_total_count() {
1531 let mut r = RouteRegistry::new();
1532 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/a", "A@index"));
1533 r.add_config_routes(&RouteConfig {
1534 routes: vec![RouteRule::new(HttpMethod::GET, "/b", "B@index")],
1535 groups: vec![],
1536 });
1537 r.add_convention_route(ConventionRoute::from_uri("/oapc/c/d").unwrap());
1538
1539 assert_eq!(r.total_count(), 3);
1540 }
1541
1542 #[test]
1547 fn test_integration_three_layer_routing() {
1548 let mut r = RouteRegistry::new();
1550 r.add_attribute_routes(vec![
1551 RouteRule::new(HttpMethod::GET, "/users", "User@list"),
1552 RouteRule::new(HttpMethod::POST, "/users", "User@create"),
1553 RouteRule::new(HttpMethod::GET, "/users/{id}", "User@show"),
1554 ]);
1555
1556 let yaml = r#"
1558routes:
1559 - method: GET
1560 path: /items
1561 handler: Item@list
1562 - method: POST
1563 path: /items
1564 handler: Item@create
1565groups:
1566 - prefix: /api/v1
1567 middleware: [auth]
1568 routes:
1569 - method: GET
1570 path: /orders
1571 handler: Order@list
1572"#;
1573 let config = load_routes_from_yaml_str(yaml).unwrap();
1574 r.add_config_routes(&config);
1575
1576 r.add_convention_route(ConventionRoute::from_uri("/oapc/customer/index").unwrap());
1578 r.add_convention_route(ConventionRoute::from_uri("/admin/login/index").unwrap());
1579
1580 assert_eq!(r.attribute_routes.len(), 3);
1582 assert_eq!(r.config_routes.len(), 3); assert_eq!(r.convention_routes.len(), 2);
1584 assert_eq!(r.total_count(), 8);
1585
1586 let merged = r.merged_rules();
1588 assert_eq!(merged.len(), 8);
1589
1590 assert!(r.attribute_conflicts().is_empty());
1592 assert!(r.config_conflicts().is_empty());
1593 }
1594
1595 #[test]
1596 fn test_integration_layer_override_priority() {
1597 let mut r = RouteRegistry::new();
1599
1600 r.add_convention_route(ConventionRoute {
1602 app: "index".to_string(),
1603 controller: "User".to_string(),
1604 action: "list".to_string(),
1605 method: HttpMethod::GET,
1606 path: "/users".to_string(),
1607 });
1608
1609 r.add_config_routes(&RouteConfig {
1611 routes: vec![RouteRule::new(HttpMethod::GET, "/users", "User@config")],
1612 groups: vec![],
1613 });
1614
1615 r.add_attribute_route(RouteRule::new(HttpMethod::GET, "/users", "User@attribute"));
1617
1618 let merged = r.merged_rules();
1619 assert_eq!(merged.len(), 1);
1620 assert_eq!(merged[0].handler, "User@attribute");
1621 }
1622}