1#[cfg(feature = "guard")]
4use crate::guards::BoxedGuard;
5use crate::lifecycle::BoxedLifecycle;
6#[cfg(feature = "middleware")]
7use crate::middleware::BoxedMiddleware;
8use crate::params::RouteParams;
9#[cfg(feature = "transition")]
10use crate::transition::TransitionConfig;
11use crate::RouteMatch;
12use gpui::{AnyElement, App, IntoElement};
13use std::collections::HashMap;
14use std::sync::Arc;
15
16#[derive(Clone, Debug, Default)]
22pub struct NamedRouteRegistry {
23 routes: HashMap<String, String>,
25}
26
27impl NamedRouteRegistry {
28 pub fn new() -> Self {
30 Self {
31 routes: HashMap::new(),
32 }
33 }
34
35 pub fn register(&mut self, name: impl Into<String>, path: impl Into<String>) {
37 self.routes.insert(name.into(), path.into());
38 }
39
40 pub fn get(&self, name: &str) -> Option<&str> {
42 self.routes.get(name).map(|s| s.as_str())
43 }
44
45 pub fn contains(&self, name: &str) -> bool {
47 self.routes.contains_key(name)
48 }
49
50 pub fn url_for(&self, name: &str, params: &RouteParams) -> Option<String> {
67 let pattern = self.get(name)?;
68 Some(substitute_params(pattern, params))
69 }
70
71 pub fn clear(&mut self) {
73 self.routes.clear();
74 }
75
76 pub fn len(&self) -> usize {
78 self.routes.len()
79 }
80
81 pub fn is_empty(&self) -> bool {
83 self.routes.is_empty()
84 }
85}
86
87fn substitute_params(pattern: &str, params: &RouteParams) -> String {
91 let mut result = pattern.to_string();
92
93 for (key, value) in params.iter() {
95 let placeholder = format!(":{}", key);
96 result = result.replace(&placeholder, value);
97 }
98
99 result
100}
101
102pub fn validate_route_path(path: &str) -> Result<(), String> {
119 if path.is_empty() {
121 return Ok(());
122 }
123
124 if path.contains("//") {
126 return Err("Route path cannot contain consecutive slashes".to_string());
127 }
128
129 let mut param_names = std::collections::HashSet::new();
134 for segment in path.split('/') {
135 if let Some(param) = segment.strip_prefix(':') {
136 if param.is_empty() {
138 return Err("Route parameter name cannot be empty".to_string());
139 }
140
141 let param_name = if let Some(pos) = param.find('{') {
143 ¶m[..pos]
144 } else {
145 param
146 };
147
148 if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
150 return Err(format!(
151 "Route parameter '{}' must contain only alphanumeric characters and underscores",
152 param_name
153 ));
154 }
155
156 if !param_names.insert(param_name.to_string()) {
158 return Err(format!("Duplicate route parameter: '{}'", param_name));
159 }
160 }
161 }
162
163 Ok(())
164}
165
166#[derive(Debug, Clone)]
172pub struct RouteConfig {
173 pub path: String,
175 pub name: Option<String>,
177 pub children: Vec<RouteConfig>,
179 pub meta: HashMap<String, String>,
181}
182
183impl RouteConfig {
184 pub fn is_layout(&self) -> bool {
186 !self.children.is_empty()
187 }
188}
189
190impl RouteConfig {
191 pub fn new(path: impl Into<String>) -> Self {
197 let path_str = path.into();
198 if let Err(e) = validate_route_path(&path_str) {
199 panic!("Invalid route path '{}': {}", path_str, e);
200 }
201 Self {
202 path: path_str,
203 name: None,
204 children: Vec::new(),
205 meta: HashMap::new(),
206 }
207 }
208
209 pub fn try_new(path: impl Into<String>) -> Result<Self, String> {
213 let path_str = path.into();
214 validate_route_path(&path_str)?;
215 Ok(Self {
216 path: path_str,
217 name: None,
218 children: Vec::new(),
219 meta: HashMap::new(),
220 })
221 }
222
223 pub fn name(mut self, name: impl Into<String>) -> Self {
225 self.name = Some(name.into());
226 self
227 }
228
229 pub fn children(mut self, children: Vec<RouteConfig>) -> Self {
231 self.children = children;
232 self
233 }
234
235 pub fn child(mut self, child: RouteConfig) -> Self {
237 self.children.push(child);
238 self
239 }
240
241 pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
243 self.meta.insert(key.into(), value.into());
244 self
245 }
246}
247
248pub type RouteBuilder = Arc<dyn Fn(&mut App, &RouteParams) -> AnyElement + Send + Sync>;
256
257pub type RouteRef = Arc<Route>;
263
264pub struct Route {
266 pub config: RouteConfig,
268 pub builder: Option<RouteBuilder>,
270 pub children: Vec<RouteRef>,
273 pub named_children: HashMap<String, Vec<RouteRef>>,
276 #[cfg(feature = "guard")]
278 pub guards: Vec<BoxedGuard>,
279 #[cfg(feature = "middleware")]
281 pub middleware: Vec<BoxedMiddleware>,
282 pub lifecycle: Option<BoxedLifecycle>,
284 #[cfg(feature = "transition")]
286 pub transition: TransitionConfig,
287}
288
289impl Route {
290 pub fn new<F, E>(path: impl Into<String>, builder: F) -> Self
314 where
315 E: IntoElement,
316 F: Fn(&mut App, &RouteParams) -> E + Send + Sync + 'static,
317 {
318 Self {
319 config: RouteConfig::new(path),
320 builder: Some(Arc::new(move |cx, params| {
321 builder(cx, params).into_any_element()
322 })),
323 children: Vec::new(),
324 named_children: HashMap::new(),
325 #[cfg(feature = "guard")]
326 guards: Vec::new(),
327 #[cfg(feature = "middleware")]
328 middleware: Vec::new(),
329 lifecycle: None,
330 #[cfg(feature = "transition")]
331 transition: TransitionConfig::default(),
332 }
333 }
334
335 pub fn children(mut self, children: Vec<RouteRef>) -> Self {
362 self.children = children;
363 self
364 }
365
366 pub fn child(mut self, child: RouteRef) -> Self {
379 self.children.push(child);
380 self
381 }
382
383 pub fn name(mut self, name: impl Into<String>) -> Self {
387 self.config.name = Some(name.into());
388 self
389 }
390
391 pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
407 self.config.meta.insert(key.into(), value.into());
408 self
409 }
410
411 pub fn named_outlet(mut self, name: impl Into<String>, children: Vec<RouteRef>) -> Self {
435 self.named_children.insert(name.into(), children);
436 self
437 }
438
439 #[cfg(feature = "guard")]
457 pub fn guard<G>(mut self, guard: G) -> Self
458 where
459 G: crate::guards::RouteGuard<
460 Future = std::pin::Pin<
461 Box<dyn std::future::Future<Output = crate::guards::GuardResult> + Send>,
462 >,
463 >,
464 {
465 self.guards.push(Box::new(guard));
466 self
467 }
468
469 #[cfg(feature = "guard")]
483 pub fn guards(mut self, guards: Vec<BoxedGuard>) -> Self {
484 self.guards.extend(guards);
485 self
486 }
487
488 #[cfg(feature = "middleware")]
502 pub fn middleware<M>(mut self, middleware: M) -> Self
503 where
504 M: crate::middleware::RouteMiddleware<
505 Future = std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
506 >,
507 {
508 self.middleware.push(Box::new(middleware));
509 self
510 }
511
512 #[cfg(feature = "middleware")]
526 pub fn middlewares(mut self, middleware: Vec<BoxedMiddleware>) -> Self {
527 self.middleware.extend(middleware);
528 self
529 }
530
531 pub fn lifecycle<L>(mut self, lifecycle: L) -> Self
547 where
548 L: crate::lifecycle::RouteLifecycle<
549 Future = std::pin::Pin<
550 Box<dyn std::future::Future<Output = crate::lifecycle::LifecycleResult> + Send>,
551 >,
552 >,
553 {
554 self.lifecycle = Some(Box::new(lifecycle));
555 self
556 }
557
558 #[cfg(feature = "transition")]
569 pub fn transition(mut self, transition: crate::transition::Transition) -> Self {
570 self.transition = TransitionConfig::new(transition);
571 self
572 }
573
574 pub fn get_named_children(&self, name: &str) -> Option<&[RouteRef]> {
578 self.named_children.get(name).map(|v| v.as_slice())
579 }
580
581 pub fn has_named_outlet(&self, name: &str) -> bool {
583 self.named_children.contains_key(name)
584 }
585
586 pub fn named_outlet_names(&self) -> Vec<&str> {
588 self.named_children.keys().map(|s| s.as_str()).collect()
589 }
590
591 pub fn matches(&self, path: &str) -> Option<RouteMatch> {
593 match_path(&self.config.path, path)
594 }
595
596 pub fn build(&self, cx: &mut App, params: &RouteParams) -> Option<AnyElement> {
598 self.builder.as_ref().map(|b| b(cx, params))
599 }
600
601 pub fn find_child(&self, segment: &str) -> Option<&RouteRef> {
605 self.children.iter().find(|child| {
606 child.config.path == segment || child.config.path.trim_start_matches('/') == segment
607 })
608 }
609
610 pub fn get_children(&self) -> &[RouteRef] {
612 &self.children
613 }
614}
615
616impl std::fmt::Debug for Route {
617 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618 f.debug_struct("Route")
619 .field("config", &self.config)
620 .field("builder", &self.builder.is_some())
621 .field("children", &self.children.len())
622 .field(
623 "named_children",
624 &self.named_children.keys().collect::<Vec<_>>(),
625 )
626 .finish()
627 }
628}
629
630fn match_path(pattern: &str, path: &str) -> Option<RouteMatch> {
637 let pattern_segments: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
638 let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
639
640 if pattern_segments.last() == Some(&"*") {
642 if path_segments.len() < pattern_segments.len() - 1 {
643 return None;
644 }
645 } else if pattern_segments.len() != path_segments.len() {
646 return None;
647 }
648
649 let mut route_match = RouteMatch::new(path.to_string());
650
651 for (i, pattern_seg) in pattern_segments.iter().enumerate() {
652 if *pattern_seg == "*" {
653 break;
655 }
656
657 if let Some(param_name) = pattern_seg.strip_prefix(':') {
658 if let Some(path_seg) = path_segments.get(i) {
660 route_match
661 .params
662 .insert(param_name.to_string(), path_seg.to_string());
663 }
664 } else if pattern_segments.get(i) != path_segments.get(i) {
665 return None;
667 }
668 }
669
670 Some(route_match)
671}
672
673pub trait IntoRoute {
696 fn into_route(self) -> RouteDescriptor;
698}
699
700pub type BuilderFn = Arc<dyn Fn(&mut App, &RouteParams) -> AnyElement + Send + Sync>;
702
703pub struct RouteDescriptor {
705 pub path: String,
707
708 pub params: RouteParams,
710
711 pub builder: Option<BuilderFn>,
713}
714
715impl IntoRoute for String {
719 fn into_route(self) -> RouteDescriptor {
720 RouteDescriptor {
721 path: self,
722 params: RouteParams::new(),
723 builder: None,
724 }
725 }
726}
727
728impl IntoRoute for &str {
730 fn into_route(self) -> RouteDescriptor {
731 RouteDescriptor {
732 path: self.to_string(),
733 params: RouteParams::new(),
734 builder: None,
735 }
736 }
737}
738
739pub struct PageRoute {
765 path: String,
766 params: RouteParams,
767 builder: Option<BuilderFn>,
768}
769
770impl PageRoute {
771 pub fn new(path: impl Into<String>) -> Self {
773 Self {
774 path: path.into(),
775 params: RouteParams::new(),
776 builder: None,
777 }
778 }
779
780 pub fn builder<F, E>(path: impl Into<String>, builder: F) -> Self
785 where
786 E: IntoElement,
787 F: Fn(&mut App, &RouteParams) -> E + Send + Sync + 'static,
788 {
789 Self {
790 path: path.into(),
791 params: RouteParams::new(),
792 builder: Some(Arc::new(move |cx, params| {
793 builder(cx, params).into_any_element()
794 })),
795 }
796 }
797
798 pub fn with_builder<F, E>(mut self, builder: F) -> Self
803 where
804 E: IntoElement,
805 F: Fn(&mut App, &RouteParams) -> E + Send + Sync + 'static,
806 {
807 self.builder = Some(Arc::new(move |cx, params| {
808 builder(cx, params).into_any_element()
809 }));
810 self
811 }
812
813 pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
815 self.params.insert(key.into(), value.into());
816 self
817 }
818
819 pub fn with_params(mut self, params: HashMap<String, String>) -> Self {
821 self.params = RouteParams::from_map(params);
822 self
823 }
824}
825
826impl IntoRoute for PageRoute {
827 fn into_route(self) -> RouteDescriptor {
828 RouteDescriptor {
829 path: self.path,
830 params: self.params,
831 builder: self.builder,
832 }
833 }
834}
835
836pub struct NamedRoute {
848 name: String,
849 params: RouteParams,
850}
851
852impl NamedRoute {
853 pub fn new(name: impl Into<String>) -> Self {
855 Self {
856 name: name.into(),
857 params: RouteParams::new(),
858 }
859 }
860
861 pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
863 self.params.insert(key.into(), value.into());
864 self
865 }
866
867 pub fn with_params(mut self, params: HashMap<String, String>) -> Self {
869 self.params = RouteParams::from_map(params);
870 self
871 }
872}
873
874impl IntoRoute for NamedRoute {
875 fn into_route(self) -> RouteDescriptor {
876 RouteDescriptor {
877 path: self.name,
878 params: self.params,
879 builder: None,
880 }
881 }
882}
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887
888 #[test]
891 fn test_registry_register_and_get() {
892 let mut registry = NamedRouteRegistry::new();
893 registry.register("home", "/");
894 registry.register("user.detail", "/users/:id");
895
896 assert_eq!(registry.get("home"), Some("/"));
897 assert_eq!(registry.get("user.detail"), Some("/users/:id"));
898 assert_eq!(registry.get("unknown"), None);
899 }
900
901 #[test]
902 fn test_registry_contains() {
903 let mut registry = NamedRouteRegistry::new();
904 registry.register("home", "/");
905
906 assert!(registry.contains("home"));
907 assert!(!registry.contains("unknown"));
908 }
909
910 #[test]
911 fn test_url_for_simple() {
912 let mut registry = NamedRouteRegistry::new();
913 registry.register("home", "/");
914
915 let params = RouteParams::new();
916 assert_eq!(registry.url_for("home", ¶ms), Some("/".to_string()));
917 }
918
919 #[test]
920 fn test_url_for_with_params() {
921 let mut registry = NamedRouteRegistry::new();
922 registry.register("user.detail", "/users/:id");
923
924 let mut params = RouteParams::new();
925 params.set("id".to_string(), "123".to_string());
926
927 assert_eq!(
928 registry.url_for("user.detail", ¶ms),
929 Some("/users/123".to_string())
930 );
931 }
932
933 #[test]
934 fn test_url_for_multiple_params() {
935 let mut registry = NamedRouteRegistry::new();
936 registry.register("post.comment", "/posts/:postId/comments/:commentId");
937
938 let mut params = RouteParams::new();
939 params.set("postId".to_string(), "42".to_string());
940 params.set("commentId".to_string(), "99".to_string());
941
942 assert_eq!(
943 registry.url_for("post.comment", ¶ms),
944 Some("/posts/42/comments/99".to_string())
945 );
946 }
947
948 #[test]
949 fn test_url_for_unknown_route() {
950 let registry = NamedRouteRegistry::new();
951 let params = RouteParams::new();
952
953 assert_eq!(registry.url_for("unknown", ¶ms), None);
954 }
955
956 #[test]
957 fn test_registry_clear() {
958 let mut registry = NamedRouteRegistry::new();
959 registry.register("home", "/");
960 registry.register("about", "/about");
961
962 assert_eq!(registry.len(), 2);
963
964 registry.clear();
965
966 assert_eq!(registry.len(), 0);
967 assert!(registry.is_empty());
968 }
969
970 #[test]
971 fn test_substitute_params() {
972 let mut params = RouteParams::new();
973 params.set("id".to_string(), "123".to_string());
974 params.set("action".to_string(), "edit".to_string());
975
976 let result = substitute_params("/users/:id/:action", ¶ms);
977 assert_eq!(result, "/users/123/edit");
978 }
979
980 #[test]
983 fn test_static_route() {
984 let result = match_path("/users", "/users");
985 assert!(result.is_some());
986
987 let result = match_path("/users", "/posts");
988 assert!(result.is_none());
989 }
990
991 #[test]
992 fn test_dynamic_route() {
993 let result = match_path("/users/:id", "/users/123");
994 assert!(result.is_some());
995
996 let route_match = result.unwrap();
997 assert_eq!(route_match.params.get("id"), Some(&"123".to_string()));
998 }
999
1000 #[test]
1001 fn test_wildcard_route() {
1002 let result = match_path("/files/*", "/files/documents/report.pdf");
1003 assert!(result.is_some());
1004
1005 let result = match_path("/files/*", "/other/path");
1006 assert!(result.is_none());
1007 }
1008
1009 #[test]
1010 fn test_string_into_route() {
1011 let route = "/users".into_route();
1012 assert_eq!(route.path, "/users");
1013 assert!(route.params.all().is_empty());
1014 }
1015
1016 #[test]
1017 fn test_material_route_with_params() {
1018 let route = PageRoute::new("/users/:id")
1019 .with_param("id", "123")
1020 .into_route();
1021
1022 assert_eq!(route.path, "/users/:id");
1023 assert_eq!(route.params.get("id"), Some(&"123".to_string()));
1024 }
1025
1026 #[test]
1027 fn test_named_route() {
1028 let route = NamedRoute::new("user_profile")
1029 .with_param("userId", "456")
1030 .into_route();
1031
1032 assert_eq!(route.path, "user_profile");
1033 assert_eq!(route.params.get("userId"), Some(&"456".to_string()));
1034 }
1035
1036 #[test]
1039 fn test_validate_valid_paths() {
1040 assert!(validate_route_path("/").is_ok());
1041 assert!(validate_route_path("/users").is_ok());
1042 assert!(validate_route_path("/users/:id").is_ok());
1043 assert!(validate_route_path("/posts/:postId/comments/:commentId").is_ok());
1044 assert!(validate_route_path("/users/:id{uuid}").is_ok());
1045 assert!(validate_route_path("/api/v1/users").is_ok());
1046 assert!(validate_route_path("settings").is_ok()); assert!(validate_route_path("").is_ok()); assert!(validate_route_path("/users/").is_ok()); }
1050
1051 #[test]
1052 fn test_validate_consecutive_slashes() {
1053 let result = validate_route_path("/users//profile");
1054 assert!(result.is_err());
1055 assert!(result.unwrap_err().contains("consecutive slashes"));
1056 }
1057
1058 #[test]
1059 fn test_validate_empty_parameter() {
1060 let result = validate_route_path("/users/:");
1061 assert!(result.is_err());
1062 assert!(result
1063 .unwrap_err()
1064 .contains("parameter name cannot be empty"));
1065 }
1066
1067 #[test]
1068 fn test_validate_invalid_parameter_name() {
1069 let result = validate_route_path("/users/:user-id");
1070 assert!(result.is_err());
1071 assert!(result.unwrap_err().contains("alphanumeric"));
1072 }
1073
1074 #[test]
1075 fn test_validate_duplicate_parameters() {
1076 let result = validate_route_path("/users/:id/posts/:id");
1077 assert!(result.is_err());
1078 assert!(result.unwrap_err().contains("Duplicate"));
1079 }
1080
1081 #[test]
1082 fn test_route_config_try_new_valid() {
1083 let result = RouteConfig::try_new("/users/:id");
1084 assert!(result.is_ok());
1085 assert_eq!(result.unwrap().path, "/users/:id");
1086 }
1087
1088 #[test]
1089 fn test_route_config_try_new_invalid() {
1090 let result = RouteConfig::try_new("/users//profile");
1091 assert!(result.is_err());
1092 }
1093
1094 #[test]
1095 #[should_panic(expected = "Invalid route path")]
1096 fn test_route_config_new_panics_on_invalid() {
1097 RouteConfig::new("/users//profile");
1098 }
1099}