//! //! Simple and stupid HTTP router. //! use std::default::Default; use std::fmt::Debug; use lazy_static::lazy_static; use regex::bytes::{Match, Regex, RegexSet}; use webdav_handler::{DavMethod, DavMethodSet}; // internal representation of a route. #[derive(Debug)] struct Route { regex: Regex, methods: Option, data: T, } /// A matched route. #[derive(Debug)] pub struct MatchedRoute<'t, 'p, T: Debug> { pub methods: Option, pub params: Vec>>, pub data: &'t T, } /// A parameter on a matched route. pub struct Param<'p>(Match<'p>); impl Debug for Param<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct("Param") .field("start", &self.0.start()) .field("end", &self.0.end()) .field("as_str", &std::str::from_utf8(self.0.as_bytes()).ok()) .finish() } } impl<'p> Param<'p> { /// Returns the starting byte offset of the match in the path. #[inline] pub fn start(&self) -> usize { self.0.start() } /// Returns the ending byte offset of the match in the path. #[inline] pub fn end(&self) -> usize { self.0.end() } /// Returns the matched part of the path. #[inline] pub fn as_bytes(&self) -> &'p [u8] { self.0.as_bytes() } /// Returns the matched part of the path as a &str, if it is valid utf-8. #[inline] pub fn as_str(&self) -> Option<&'p str> { std::str::from_utf8(self.0.as_bytes()).ok() } } pub struct Builder { routes: Vec>, } impl Builder { /// Add a route. /// /// Routes are matched in the order they were added. /// /// If a route starts with '^', it's assumed that it is a regular /// expression. Parameters are included as "named capture groups". /// /// Otherwise, it's a route-expression, with just the normal :params /// and *splat param, and parts between parentheses are optional. /// /// Example: /// /// - /api/get/:id /// - /files/*path /// - /users(/) /// - /users(/*path) /// pub fn add( &mut self, route: impl AsRef, methods: Option, data: T, ) -> Result<&mut Self, regex::Error> { let route = route.as_ref(); // Might be a regexp if route.starts_with("^") { return self.add_re(route, methods, data); } // Ignore it if it does not start with / if !route.starts_with("/") { return Ok(self); } // First, replace special characters "()*" with unicode chars // from the private-use area, so that we can then regex-escape // the entire string. let re_route = route .chars() .map(|c| { match c { '*' => '\u{e001}', '(' => '\u{e002}', ')' => '\u{e003}', '\u{e001}' => ' ', '\u{e002}' => ' ', '\u{e003}' => ' ', c => c, } }) .collect::(); let re_route = regex::escape(&re_route); // Translate route expression into regexp. // We do a simple transformation: // :ident -> (?P[^/]*) // *ident -> (?P.*) // (text) -> (?:text|) lazy_static! { static ref COLON: Regex = Regex::new(":([a-zA-Z0-9]+)").unwrap(); static ref SPLAT: Regex = Regex::new("\u{e001}([a-zA-Z0-9]+)").unwrap(); static ref MAYBE: Regex = Regex::new("\u{e002}([^\u{e002}]*)\u{e003}").unwrap(); }; let mut re_route = re_route.into_bytes(); re_route = COLON.replace_all(&re_route, &b"(?P<$1>[^/]*)"[..]).to_vec(); re_route = SPLAT.replace_all(&re_route, &b"(?P<$1>.*)"[..]).to_vec(); re_route = MAYBE.replace_all(&re_route, &b"($1)?"[..]).to_vec(); // finalize regex. let re_route = "^".to_string() + &String::from_utf8(re_route).unwrap() + "$"; self.add_re(&re_route, methods, data) } // add route as regular expression. fn add_re(&mut self, s: &str, methods: Option, data: T) -> Result<&mut Self, regex::Error> { // Set flags: enable ". matches everything", disable strict unicode. // We known 's' starts with "^", add it after that. let s2 = format!("^(?s){}", &s[1..]); let regex = Regex::new(&s2)?; self.routes.push(Route { regex, methods, data }); Ok(self) } /// Combine all the routes and compile them into an internal RegexSet. pub fn build(&mut self) -> Router { let set = RegexSet::new(self.routes.iter().map(|r| r.regex.as_str())).unwrap(); Router { routes: std::mem::replace(&mut self.routes, Vec::new()), set, } } } /// Dead simple HTTP router. #[derive(Debug)] pub struct Router { set: RegexSet, routes: Vec>, } impl Default for Router { fn default() -> Router { Router { set: RegexSet::new(&[] as &[&str]).unwrap(), routes: Vec::new(), } } } impl Router { /// Return a builder. pub fn builder() -> Builder { Builder { routes: Vec::new() } } /// See if the path matches a route in the set. /// /// The names of the parameters you want to be returned need to be passed in as an array. pub fn matches<'a>( &self, path: &'a [u8], method: DavMethod, param_names: &[&str], ) -> Vec> { let mut matched = Vec::new(); for idx in self.set.matches(path) { let route = &self.routes[idx]; if route.methods.map(|m| m.contains(method)).unwrap_or(true) { let mut params = Vec::new(); if let Some(caps) = route.regex.captures(path) { for name in param_names { params.push(caps.name(name).map(|p| Param(p))); } } else { for _ in param_names { params.push(None); } } matched.push(MatchedRoute { methods: route.methods, params, data: &route.data, }); } } matched } } #[cfg(test)] mod tests { use super::*; use webdav_handler::DavMethod; fn test_match(rtr: &Router, p: &[u8], user: &str, path: &str) { let x = rtr.matches(p, DavMethod::Get, &["user", "path"]); assert!(x.len() > 0); let x = &x[0]; if user != "" { assert!(x.params[0] .as_ref() .map(|b| b.as_bytes() == user.as_bytes()) .unwrap_or(false)); } if path != "" { assert!(x.params[1] .as_ref() .map(|b| b.as_bytes() == path.as_bytes()) .unwrap_or(false)); } } #[test] fn test_router() -> Result<(), Box> { let rtr = Router::::builder() .add("/", None, 1)? .add("/users(/:user)", None, 2)? .add("/files/*path", None, 3)? .add("/files(/*path)", None, 4)? .build(); test_match(&rtr, b"/", "", ""); test_match(&rtr, b"/users", "", ""); test_match(&rtr, b"/users/", "", ""); test_match(&rtr, b"/users/mike", "mike", ""); test_match(&rtr, b"/files/foo/bar", "", "foo/bar"); test_match(&rtr, b"/files", "", ""); Ok(()) } }