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