use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Ord, Eq)]
pub enum RouteType {
Static,
Dynamic,
Wildcard,
}
#[derive(Clone)]
pub enum RouteMatch<T: Clone> {
Matched {
path: String,
handler: T,
params: HashMap<String, String>,
},
NotMatched,
}
#[derive(Clone)]
pub struct Route<T: Clone> {
pub path: String,
pub parts: Vec<String>,
pub handler: T,
pub route_type: RouteType,
}
pub struct Router<T: Clone> {
routes: RwLock<Vec<Route<T>>>,
}
impl<T: Clone> Router<T> {
pub fn new() -> Self {
Router {
routes: RwLock::new(vec![]),
}
}
pub fn add(&self, path: impl Into<String>, handler: T) {
let path = path.into();
let route_type = if path.contains("*") {
RouteType::Wildcard
} else if path.contains(":") {
RouteType::Dynamic
} else {
RouteType::Static
};
let parts = path
.trim_matches('/')
.split('/')
.map(|s| s.to_string())
.collect();
let mut wg = self.routes.write().unwrap();
wg.push(Route {
path,
parts,
handler,
route_type,
});
wg.sort_by(|a, b| a.route_type.cmp(&b.route_type))
}
pub fn remove(&self, path: &str) -> Option<Route<T>> {
let mut wg = self.routes.write().unwrap();
for i in (0..wg.len()).rev() {
if path.cmp(&wg[i].path) == Ordering::Equal {
return Some(wg.remove(i));
}
}
None
}
pub fn go_match(&self, path: &str) -> RouteMatch<T> {
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
let routes = {
let wg = self.routes.read().unwrap();
let routes: Vec<Route<T>> = wg.clone();
routes
};
for route in &routes {
let route_parts: Vec<&str> = route.parts.iter().map(|s| s as &str).collect();
let mut params = HashMap::new();
if route_parts.len() != parts.len()
&& !route_parts.iter().any(|part| part.starts_with('*'))
{
continue;
}
let mut matched = true;
for (i, route_part) in route_parts.iter().enumerate() {
if *route_part == "*" || route_part.starts_with('*') {
let key = route_part.trim_start_matches('*');
params.insert(key.to_string(), parts[i..].join("/"));
break;
} else if route_part.starts_with(':') {
let key = route_part.trim_start_matches(':');
if let Some(val) = parts.get(i) {
params.insert(key.to_string(), val.to_string());
} else {
matched = false;
break;
}
} else if let Some(req_part) = parts.get(i) {
if route_part != req_part {
matched = false;
break;
}
} else {
matched = false;
break;
}
}
if matched {
return RouteMatch::Matched {
path: route.path.clone(),
handler: route.handler.clone(),
params,
};
}
}
RouteMatch::NotMatched
}
}
impl<T: Clone> Debug for RouteMatch<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RouteMatch::Matched {
path,
handler: _,
params,
} => f
.debug_struct("Matched")
.field("path", path)
.field("params", params)
.finish(),
RouteMatch::NotMatched => f.write_str("NotMatched"),
}
}
}
impl<T: Clone> Debug for Route<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Route")
.field("path", &self.path)
.field("parts", &self.parts)
.field("route_type", &self.route_type)
.finish()
}
}
#[test]
fn test() {
let r = Router::<()>::new();
r.add("/static", ());
r.add("/home/*", ());
r.add("/dynamic/:page", ());
println!("{:?}", r.go_match("/static"));
println!("{:?}", r.go_match("/static2"));
println!("{:?}", r.go_match("/dynamic/wo"));
println!("{:?}", r.go_match("/home/path1/path2"));
println!("{:?}", r.remove("/home"));
println!("{:?}", r.remove("/home/*"));
}