use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteMatch {
pub params: BTreeMap<String, String>,
}
pub fn match_path(pattern: &str, path: &str) -> Option<RouteMatch> {
let pattern_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if pattern_parts.last() == Some(&"*") {
let prefix = &pattern_parts[..pattern_parts.len() - 1];
if path_parts.len() < prefix.len() {
return None;
}
let mut params = BTreeMap::new();
for (pp, rp) in prefix.iter().zip(path_parts.iter()) {
if let Some(name) = pp.strip_prefix(':') {
params.insert(name.to_string(), rp.to_string());
} else if pp != rp {
return None;
}
}
return Some(RouteMatch { params });
}
if pattern_parts.len() != path_parts.len() {
return None;
}
let mut params = BTreeMap::new();
for (pp, rp) in pattern_parts.iter().zip(path_parts.iter()) {
if let Some(name) = pp.strip_prefix(':') {
params.insert(name.to_string(), rp.to_string());
} else if pp != rp {
return None;
}
}
Some(RouteMatch { params })
}
#[cfg(test)]
mod tests {
use super::*;
fn m(pattern: &str, path: &str) -> Option<Vec<(String, String)>> {
match_path(pattern, path).map(|m| m.params.into_iter().collect())
}
#[test]
fn exact_match() {
assert_eq!(m("/dashboard", "/dashboard"), Some(vec![]));
assert_eq!(m("/dashboard", "/other"), None);
}
#[test]
fn single_param() {
assert_eq!(
m("/users/:id", "/users/42"),
Some(vec![("id".into(), "42".into())])
);
}
#[test]
fn multiple_params() {
assert_eq!(
m("/users/:id/posts/:postId", "/users/42/posts/99"),
Some(vec![
("id".into(), "42".into()),
("postId".into(), "99".into()),
])
);
}
#[test]
fn wildcard_matches_prefix_and_descendants() {
assert!(match_path("/api/*", "/api").is_some());
assert!(match_path("/api/*", "/api/users").is_some());
assert!(match_path("/api/*", "/api/users/42").is_some());
assert!(match_path("/api/*", "/other").is_none());
}
#[test]
fn wildcard_preserves_leading_params() {
let matched = match_path("/users/:id/*", "/users/42/posts/99").unwrap();
assert_eq!(matched.params.get("id"), Some(&"42".to_string()));
}
#[test]
fn rejects_length_mismatch() {
assert!(match_path("/users/:id", "/users").is_none());
assert!(match_path("/users/:id", "/users/42/extra").is_none());
}
#[test]
fn trailing_slash_is_ignored() {
assert!(match_path("/users/:id", "/users/42/").is_some());
assert!(match_path("/users/:id/", "/users/42").is_some());
}
#[test]
fn root_matches() {
assert!(match_path("/", "/").is_some());
assert!(match_path("", "/").is_some());
}
}