euv_ui/component/router/fn.rs
1use super::*;
2
3/// Normalizes a path by stripping a trailing `/` (except
4/// for the root path `/`, which stays as `/`).
5///
6/// # Arguments
7///
8/// - `&str` - Shared reference to a `str`.
9///
10/// # Returns
11///
12/// - `String` - A `String` value.
13pub fn normalize_path(path: &str) -> String {
14 if path == "/" {
15 return "/".to_string();
16 }
17 path.trim_end_matches('/').to_string()
18}
19
20/// Returns `true` if `path` matches the route's pattern.
21///
22/// Matching rules:
23/// - Exact match after normalization wins.
24/// - For a parent route with children, a path that
25/// starts with `route.path + "/"` matches the parent
26/// (so `/settings` matches `/settings/profile`).
27/// - The root route `/` matches everything except the
28/// empty string.
29///
30/// # Arguments
31///
32/// - `&str` - Shared reference to a `str`.
33/// - `&str` - Shared reference to a `str`.
34///
35/// # Returns
36///
37/// - `bool` - A boolean.
38pub fn route_matches(route_path: &str, request_path: &str) -> bool {
39 let normalized_route: String = normalize_path(route_path);
40 let normalized_request: String = normalize_path(request_path);
41 if normalized_route == normalized_request {
42 return true;
43 }
44 // Parent-route match: request starts with
45 // route_path + "/".
46 if normalized_request.starts_with(&normalized_route)
47 && normalized_request.chars().nth(normalized_route.len()) == Some('/')
48 {
49 return true;
50 }
51 // Root catch-all.
52 if normalized_route == "/" && !normalized_request.is_empty() {
53 return true;
54 }
55 false
56}
57
58/// Recursively finds the deepest matching route in the
59/// configuration tree.
60///
61/// Exact matches on children win over parent-prefix
62/// matches. If multiple children match, the first one
63/// in declaration order wins (matching `euv_routes`'s
64/// existing "first match wins" semantics).
65///
66/// Returns `None` if no route matches.
67///
68/// # Arguments
69///
70/// - `&str` - Shared reference to a `str`.
71/// - `&'a [NestedRouteConfig]` - Shared reference to a `'a [NestedRouteConfig]`.
72///
73/// # Returns
74///
75/// - `Option<'a NestedRouteConfig>` - `Some(...)` on success, `None` otherwise.
76pub fn find_active_route<'a>(
77 path: &str,
78 routes: &'a [NestedRouteConfig],
79) -> Option<&'a NestedRouteConfig> {
80 // First pass: exact match wins.
81 for route in routes.iter() {
82 let normalized: String = normalize_path(&route.path);
83 let normalized_request: String = normalize_path(path);
84 if normalized == normalized_request {
85 // Try to find a deeper match in the
86 // children of this route.
87 if !route.children.is_empty()
88 && let Some(child_match) = find_active_route(path, &route.children)
89 {
90 return Some(child_match);
91 }
92 return Some(route);
93 }
94 }
95 // Second pass: parent-prefix match. Walk all
96 // routes; for each, if it could be a parent of the
97 // request, recurse into its children. If a child
98 // matches, return the child. If no child matches,
99 // return the parent.
100 for route in routes.iter() {
101 if route_matches(&route.path, path) && route.path != normalize_path(path) {
102 if !route.children.is_empty()
103 && let Some(child_match) = find_active_route(path, &route.children)
104 {
105 return Some(child_match);
106 }
107 return Some(route);
108 }
109 }
110 None
111}
112
113/// Returns the chain of routes from the root to the
114/// matched route, in render order (root first).
115///
116/// This is the "breadcrumb" chain that the layout
117/// component walks to render `<Outlet>` for each parent.
118/// Returns an empty vec if no route matches.
119///
120/// # Arguments
121///
122/// - `&str` - Shared reference to a `str`.
123/// - `&'a [NestedRouteConfig]` - Shared reference to a `'a [NestedRouteConfig]`.
124///
125/// # Returns
126///
127/// - `Vec<'a NestedRouteConfig>` - A `Vec<'a NestedRouteConfig>` value.
128pub fn route_chain<'a>(path: &str, routes: &'a [NestedRouteConfig]) -> Vec<&'a NestedRouteConfig> {
129 let mut chain: Vec<&'a NestedRouteConfig> = Vec::new();
130 build_chain(path, routes, &mut chain);
131 chain
132}
133
134/// Recursively walks the route tree looking for the deepest
135/// match for `path`. Each matching route is appended to
136/// `chain` in nesting order, so the layout component can
137/// render `<Outlet>` for every parent. Returns `true` as
138/// soon as a match is appended so callers can short-circuit
139/// the second pass.
140///
141/// # Arguments
142///
143/// - `&str` - The current request path.
144/// - `&'a [NestedRouteConfig]` - The slice of routes to search.
145/// - `&mut Vec<&'a NestedRouteConfig>` - Out-parameter that
146/// accumulates the matched parent chain.
147///
148/// # Returns
149///
150/// - `bool` - `true` when a route was appended to `chain`,
151/// `false` otherwise.
152pub(crate) fn build_chain<'a>(
153 path: &str,
154 routes: &'a [NestedRouteConfig],
155 chain: &mut Vec<&'a NestedRouteConfig>,
156) -> bool {
157 // First pass: exact match.
158 for route in routes.iter() {
159 let normalized: String = normalize_path(&route.path);
160 let normalized_request: String = normalize_path(path);
161 if normalized == normalized_request {
162 chain.push(route);
163 // Recurse into children for deeper matches.
164 if !route.children.is_empty() {
165 let _ = build_chain(path, &route.children, chain);
166 }
167 return true;
168 }
169 }
170 // Second pass: parent-prefix match.
171 for route in routes.iter() {
172 if route_matches(&route.path, path) && route.path != normalize_path(path) {
173 chain.push(route);
174 if !route.children.is_empty() {
175 let _ = build_chain(path, &route.children, chain);
176 }
177 return true;
178 }
179 }
180 false
181}