use std::collections::HashMap;
use smallvec::SmallVec;
#[derive(Debug, Clone)]
pub struct CompiledPath {
segments: SmallVec<[PathSegment; 8]>,
param_count: usize,
}
#[derive(Debug, Clone)]
enum PathSegment {
Literal(String),
Parameter(String),
}
impl CompiledPath {
pub fn new(pattern: &str) -> Self {
let segments: SmallVec<[PathSegment; 8]> = pattern
.split('/')
.filter(|s| !s.is_empty())
.map(|segment| {
if let Some(param_name) = segment.strip_prefix(':') {
PathSegment::Parameter(param_name.to_string())
} else {
PathSegment::Literal(segment.to_string())
}
})
.collect();
let param_count = segments
.iter()
.filter(|s| matches!(s, PathSegment::Parameter(_)))
.count();
CompiledPath { segments, param_count }
}
pub fn matches(&self, path: &str) -> bool {
let path_segments: SmallVec<[&str; 8]> = path
.split('/')
.filter(|s| !s.is_empty())
.collect();
if self.segments.len() != path_segments.len() {
return false;
}
self.segments
.iter()
.zip(path_segments.iter())
.all(|(pattern_seg, path_seg)| match pattern_seg {
PathSegment::Literal(literal) => literal == path_seg,
PathSegment::Parameter(_) => true,
})
}
pub fn extract_params(&self, path: &str) -> Option<HashMap<String, String>> {
let path_segments: SmallVec<[&str; 8]> = path
.split('/')
.filter(|s| !s.is_empty())
.collect();
if self.segments.len() != path_segments.len() {
return None;
}
let mut params = HashMap::with_capacity(self.param_count);
for (pattern_seg, path_seg) in self.segments.iter().zip(path_segments.iter()) {
match pattern_seg {
PathSegment::Parameter(name) => {
params.insert(name.clone(), (*path_seg).to_string());
}
PathSegment::Literal(literal) if literal != path_seg => return None,
_ => {}
}
}
Some(params)
}
pub fn pattern_key(&self) -> String {
self.segments
.iter()
.map(|seg| match seg {
PathSegment::Literal(s) => s.as_str(),
PathSegment::Parameter(_) => ":param",
})
.collect::<Vec<_>>()
.join("/")
}
}
#[derive(Debug, Clone)]
pub struct PathRouter<T> {
routes: Vec<(CompiledPath, T)>,
}
impl<T: Clone> PathRouter<T> {
pub fn new() -> Self {
PathRouter { routes: Vec::new() }
}
pub fn add_route(&mut self, pattern: &str, handler: T) {
let compiled = CompiledPath::new(pattern);
self.routes.push((compiled, handler));
}
pub fn find_match(&self, path: &str) -> Option<(&T, HashMap<String, String>)> {
self.routes
.iter()
.find_map(|(compiled_path, handler)| {
compiled_path.extract_params(path)
.map(|params| (handler, params))
})
}
}
impl<T: Clone> Default for PathRouter<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compiled_path_matching() {
let pattern = CompiledPath::new("/users/:id/posts/:post_id");
assert!(pattern.matches("/users/123/posts/456"));
assert!(!pattern.matches("/users/123"));
assert!(!pattern.matches("/users/123/posts/456/comments"));
}
#[test]
fn test_compiled_path_params() {
let pattern = CompiledPath::new("/users/:id/posts/:post_id");
let params = pattern.extract_params("/users/123/posts/456").unwrap();
assert_eq!(params.get("id"), Some(&"123".to_string()));
assert_eq!(params.get("post_id"), Some(&"456".to_string()));
}
#[test]
fn test_path_router() {
let mut router = PathRouter::new();
router.add_route("/users/:id", "user_handler");
router.add_route("/posts/:id", "post_handler");
router.add_route("/users/:user_id/posts/:post_id", "user_post_handler");
let (handler, params) = router.find_match("/users/123").unwrap();
assert_eq!(*handler, "user_handler");
assert_eq!(params.get("id"), Some(&"123".to_string()));
let (handler, params) = router.find_match("/users/123/posts/456").unwrap();
assert_eq!(*handler, "user_post_handler");
assert_eq!(params.get("user_id"), Some(&"123".to_string()));
assert_eq!(params.get("post_id"), Some(&"456".to_string()));
}
#[test]
fn test_empty_path_segments() {
let pattern = CompiledPath::new("/users//posts/");
assert!(pattern.matches("/users/posts"));
let pattern2 = CompiledPath::new("users/:id");
assert!(pattern2.matches("users/123"));
}
}