Skip to main content

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}