#![allow(clippy::unnecessary_unwrap, clippy::ptr_arg)]
use rustc_hash::FxHashMap;
use crate::RouteView;
#[derive(Clone, Debug, Default)]
pub struct RouteParams(pub FxHashMap<String, String>);
impl RouteParams {
pub fn new() -> Self {
Self(FxHashMap::default())
}
pub fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).map(|s| s.as_str())
}
pub fn get_parsed<T: std::str::FromStr>(&self, key: &str) -> Option<T> {
self.0.get(key).and_then(|s| s.parse().ok())
}
pub fn insert(&mut self, key: String, value: String) {
self.0.insert(key, value);
}
}
#[derive(Clone, Debug, Default)]
pub struct QueryParams(pub FxHashMap<String, String>);
impl QueryParams {
pub fn new() -> Self {
Self(FxHashMap::default())
}
pub fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).map(|s| s.as_str())
}
pub fn parse(query: &str) -> Self {
let mut params = FxHashMap::default();
for pair in query.split('&') {
if let Some((key, value)) = pair.split_once('=') {
params.insert(key.to_string(), value.to_string());
}
}
Self(params)
}
}
pub struct RouteContext {
pub params: RouteParams,
pub query: QueryParams,
pub path: String,
pub router: crate::Router,
}
pub struct Route {
pub path: String,
pub name: Option<String>,
pub view: Option<RouteView>,
pub children: Vec<Route>,
pub guards: Vec<crate::NavigationGuard>,
pub transition: Option<crate::transition::PageTransition>,
}
impl Route {
pub fn new(path: impl Into<String>) -> Self {
Self {
path: path.into(),
name: None,
view: None,
children: Vec::new(),
guards: Vec::new(),
transition: None,
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn view(mut self, view: RouteView) -> Self {
self.view = Some(view);
self
}
pub fn child(mut self, child: Route) -> Self {
self.children.push(child);
self
}
pub fn transition(mut self, transition: crate::transition::PageTransition) -> Self {
self.transition = Some(transition);
self
}
pub fn guard(mut self, guard: crate::NavigationGuard) -> Self {
self.guards.push(guard);
self
}
}
#[derive(Clone, Debug)]
pub struct MatchedRoute {
pub path: String,
pub name: Option<String>,
pub params: RouteParams,
pub query: QueryParams,
pub view_index: usize,
pub transition: Option<crate::transition::PageTransition>,
}
#[derive(Clone, Debug)]
enum SegmentType {
Static(String),
Param(String),
Wildcard(String),
}
struct TrieNode {
segment: SegmentType,
children: Vec<TrieNode>,
route_index: Option<usize>,
route_name: Option<String>,
route_transition: Option<crate::transition::PageTransition>,
}
pub struct RouteTrie {
roots: Vec<TrieNode>,
not_found_index: Option<usize>,
}
impl Default for RouteTrie {
fn default() -> Self {
Self::new()
}
}
impl RouteTrie {
pub fn new() -> Self {
Self {
roots: Vec::new(),
not_found_index: None,
}
}
pub fn add(
&mut self,
path: &str,
route_index: usize,
name: Option<&str>,
transition: Option<crate::transition::PageTransition>,
) {
if path == "/" {
self.add_root(route_index, name, transition);
return;
}
let segments = parse_segments(path);
let mut current_children = &mut self.roots;
for seg in &segments {
let pos = current_children
.iter()
.position(|n| segment_matches_type(&n.segment, seg));
if let Some(pos) = pos {
current_children = &mut current_children[pos].children;
} else {
let node = TrieNode {
segment: seg.clone(),
children: Vec::new(),
route_index: None,
route_name: None,
route_transition: None,
};
current_children.push(node);
let last = current_children.len() - 1;
current_children = &mut current_children[last].children;
}
}
let terminal = find_terminal_mut(&mut self.roots, &segments);
if let Some(node) = terminal {
node.route_index = Some(route_index);
node.route_name = name.map(|s| s.to_string());
node.route_transition = transition;
}
}
pub fn set_not_found(&mut self, index: usize) {
self.not_found_index = Some(index);
}
fn add_root(
&mut self,
route_index: usize,
name: Option<&str>,
transition: Option<crate::transition::PageTransition>,
) {
let node = TrieNode {
segment: SegmentType::Static(String::new()),
children: Vec::new(),
route_index: Some(route_index),
route_name: name.map(|s| s.to_string()),
route_transition: transition,
};
self.roots.push(node);
}
pub fn match_path(&self, path: &str) -> Option<MatchedRoute> {
let (path_part, query_str) = path.split_once('?').unwrap_or((path, ""));
let query = QueryParams::parse(query_str);
let segments: Vec<&str> = path_part.split('/').filter(|s| !s.is_empty()).collect();
if segments.is_empty() {
for node in &self.roots {
if let SegmentType::Static(ref s) = node.segment {
if s.is_empty() && node.route_index.is_some() {
return Some(MatchedRoute {
path: "/".to_string(),
name: node.route_name.clone(),
params: RouteParams::new(),
query,
view_index: node.route_index.unwrap(),
transition: node.route_transition.clone(),
});
}
}
}
}
let mut params = RouteParams::new();
if let Some(matched) = match_recursive(&self.roots, &segments, 0, &mut params) {
Some(MatchedRoute {
path: path_part.to_string(),
name: matched.1,
params,
query,
view_index: matched.0,
transition: matched.2,
})
} else {
self.not_found_index.map(|nf_idx| MatchedRoute {
path: path_part.to_string(),
name: Some("not_found".to_string()),
params: RouteParams::new(),
query,
view_index: nf_idx,
transition: None,
})
}
}
}
fn parse_segments(path: &str) -> Vec<SegmentType> {
path.split('/')
.filter(|s| !s.is_empty())
.map(|s| {
if let Some(name) = s.strip_prefix(':') {
SegmentType::Param(name.to_string())
} else if let Some(name) = s.strip_prefix('*') {
SegmentType::Wildcard(name.to_string())
} else {
SegmentType::Static(s.to_string())
}
})
.collect()
}
fn segment_matches_type(existing: &SegmentType, new: &SegmentType) -> bool {
matches!(
(existing, new),
(SegmentType::Static(a), SegmentType::Static(b)) if a == b
) || matches!(
(existing, new),
(SegmentType::Param(a), SegmentType::Param(b)) if a == b
) || matches!(
(existing, new),
(SegmentType::Wildcard(a), SegmentType::Wildcard(b)) if a == b
)
}
fn find_terminal_mut<'a>(
nodes: &'a mut Vec<TrieNode>,
segments: &[SegmentType],
) -> Option<&'a mut TrieNode> {
if segments.is_empty() {
return None;
}
let seg = &segments[0];
for node in nodes.iter_mut() {
if segment_matches_type(&node.segment, seg) {
if segments.len() == 1 {
return Some(node);
}
return find_terminal_mut(&mut node.children, &segments[1..]);
}
}
None
}
fn match_recursive(
nodes: &[TrieNode],
segments: &[&str],
depth: usize,
params: &mut RouteParams,
) -> Option<(
usize,
Option<String>,
Option<crate::transition::PageTransition>,
)> {
if depth >= segments.len() {
return None;
}
let segment = segments[depth];
for node in nodes {
if let SegmentType::Static(ref s) = node.segment {
if s == segment {
if depth + 1 == segments.len() && node.route_index.is_some() {
return Some((
node.route_index.unwrap(),
node.route_name.clone(),
node.route_transition.clone(),
));
}
if let Some(result) = match_recursive(&node.children, segments, depth + 1, params) {
return Some(result);
}
}
}
}
for node in nodes {
if let SegmentType::Param(ref name) = node.segment {
params.insert(name.clone(), segment.to_string());
if depth + 1 == segments.len() && node.route_index.is_some() {
return Some((
node.route_index.unwrap(),
node.route_name.clone(),
node.route_transition.clone(),
));
}
if let Some(result) = match_recursive(&node.children, segments, depth + 1, params) {
return Some(result);
}
params.0.remove(name);
}
}
for node in nodes {
if let SegmentType::Wildcard(ref name) = node.segment {
let rest = segments[depth..].join("/");
params.insert(name.clone(), rest);
if node.route_index.is_some() {
return Some((
node.route_index.unwrap(),
node.route_name.clone(),
node.route_transition.clone(),
));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_static_route() {
let mut trie = RouteTrie::new();
trie.add("/about", 0, Some("about"), None);
trie.add("/users", 1, Some("users"), None);
let m = trie.match_path("/about").unwrap();
assert_eq!(m.view_index, 0);
assert_eq!(m.name.as_deref(), Some("about"));
let m = trie.match_path("/users").unwrap();
assert_eq!(m.view_index, 1);
assert!(trie.match_path("/missing").is_none());
}
#[test]
fn test_param_route() {
let mut trie = RouteTrie::new();
trie.add("/users/:id", 0, None, None);
let m = trie.match_path("/users/42").unwrap();
assert_eq!(m.view_index, 0);
assert_eq!(m.params.get("id"), Some("42"));
}
#[test]
fn test_nested_route() {
let mut trie = RouteTrie::new();
trie.add("/users/:id/posts", 0, None, None);
let m = trie.match_path("/users/42/posts").unwrap();
assert_eq!(m.view_index, 0);
assert_eq!(m.params.get("id"), Some("42"));
}
#[test]
fn test_query_params() {
let mut trie = RouteTrie::new();
trie.add("/search", 0, None, None);
let m = trie.match_path("/search?q=hello&page=2").unwrap();
assert_eq!(m.view_index, 0);
assert_eq!(m.query.get("q"), Some("hello"));
assert_eq!(m.query.get("page"), Some("2"));
}
#[test]
fn test_not_found() {
let mut trie = RouteTrie::new();
trie.add("/home", 0, None, None);
trie.set_not_found(99);
let m = trie.match_path("/missing").unwrap();
assert_eq!(m.view_index, 99);
}
}