Skip to main content

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}