/* Copyright (c) 2016 * Jeff Nettleton * * Licensed under the MIT license (http://opensource.org/licenses/MIT). This * file may not be copied, modified, or distributed except according to those * terms */ extern crate regex; use std::collections::HashMap; use regex::Regex; use crate::request::*; use crate::response::*; // The various types of parameters that can be contained in a URI. #[derive(PartialEq, Eq, Hash, Debug)] enum ParamType { Integer, Unsigned, String, Float, Path, } /// This struct represents a route definition. It is only necessary for /// use internally. #[derive(PartialEq, Eq, Hash, Debug, Clone)] pub struct RouteDef { pub pathdef: String, pub method: Method, } /// This struct defines a route or endpoint. pub struct Route { matcher: Regex, method: Method, params: HashMap, pub handler: fn(&Request) -> Response, } impl Route { /// Create a new Route. This function is called by the Canteen struct. pub fn new(path: &str, method: Method, handler: fn(&Request) -> Response) -> Route { let re = Regex::new(r"^<(?:(int|uint|str|float|path):)?([\w_][a-zA-Z0-9_]*)>$").unwrap(); let parts: Vec<&str> = path.split('/').filter(|&s| s != "").collect(); let mut matcher: String = String::from(r"^"); let mut params: HashMap = HashMap::new(); for part in parts { let chunk: String = if re.is_match(part) { let caps = re.captures(part).unwrap(); let param = caps.get(2).unwrap().as_str(); let ptype: ParamType = match caps.get(1) { Some(x) => { match x.as_str() { "int" => ParamType::Integer, "uint" => ParamType::Unsigned, "float" => ParamType::Float, "path" => ParamType::Path, "str" => ParamType::String, _ => ParamType::String, } } None => ParamType::String, }; let mstr: String = match ptype { ParamType::String => String::from(r"(?:[^/])+"), ParamType::Integer => String::from(r"-*[0-9]+"), ParamType::Unsigned => String::from(r"[0-9]+"), ParamType::Float => String::from(r"-*[0-9]*[.]?[0-9]+"), ParamType::Path => String::from(r".+"), }; params.insert(String::from(param), ptype); format!("/(?P<{}>{})", ¶m, &mstr) } else { String::from("/") + part }; matcher.push_str(&chunk); } /* end the regex with an optional final slash and a string terminator */ matcher.push_str("/?$"); Route { matcher: Regex::new(&matcher).unwrap(), params, method, handler, } } /// Check if this Route matches a given URI. pub fn is_match(&self, req: &Request) -> bool { self.matcher.is_match(&req.path) && self.method == req.method } /// Parse and extract the variables from a URI based on this Route's definition. pub fn parse(&self, path: &str) -> HashMap { let mut params: HashMap = HashMap::new(); if self.matcher.is_match(&path) { let caps = self.matcher.captures(path).unwrap(); for param in self.params.keys() { params.insert(param.clone(), String::from(caps.name(¶m).unwrap().as_str())); } } params } } #[cfg(test)] mod tests { use super::*; use crate::utils; #[test] fn test_route_match() { let rt = Route::new("/api/v1/foo/", Method::Get, utils::err_404); let mut req = Request::new(); let cases = vec![ (String::from("/api/v1/bar"), Method::Get, false), (String::from("/api/v1/foo"), Method::Post, false), (String::from("/api/v1/foo/asdf"), Method::Get, false), (String::from("/api/va/foo/1.23"), Method::Get, false), (String::from("/api/v1/foo/123"), Method::Post, false), (String::from("/api/v1/foo/123"), Method::Get, true), ]; for (path, method, success) in cases.into_iter() { req.path = path; req.method = method; assert_eq!(success, rt.is_match(&req)); } } #[test] fn test_route_match_simple() { let route = Route::new("/api/v1/foo/", Method::Get, utils::err_404); let parsed = route.parse("/api/v1/foo/blahblahblah"); assert_eq!("blahblahblah", parsed.get("foo_stuff").unwrap()); } #[test] fn test_route_match_single_int() { let route = Route::new("/api/v1/foo/", Method::Get, utils::err_404); let parsed = route.parse("/api/v1/foo/123"); assert_eq!("123", parsed.get("foo_id").unwrap()); } #[test] fn test_route_match_single_uint() { let route = Route::new("/api/v1/foo/", Method::Get, utils::err_404); let parsed = route.parse("/api/v1/foo/123"); let mut badreq = Request::new(); badreq.method = Method::Get; badreq.path = String::from("/api/v1/foo/-123"); assert_eq!("123", parsed.get("foo_id").unwrap()); assert_eq!(false, route.is_match(&badreq)); } #[test] fn test_route_match_single_str() { let rt = Route::new("/api/v1/foo/", Method::Get, utils::err_404); assert_eq!("blahblahblah", rt.parse("/api/v1/foo/blahblahblah").get("foo_stuff").unwrap()); } #[test] fn test_route_match_many() { let rt = Route::new("/api/v1/foo//bar//baz/", Method::Get, utils::err_404); let rm = rt.parse("/api/v1/foo/123/bar/bar/baz/456"); assert_eq!("123", rm.get("foo_id").unwrap()); assert_eq!("bar", rm.get("bar").unwrap()); assert_eq!("456", rm.get("baz_id").unwrap()); } #[test] fn test_find_route_native_types() { let mut request = Request::new(); let routes = vec![Route::new("/api/v1/foo/", Method::Get, utils::err_404), Route::new("/api/v1/foo//bar/", Method::Get, utils::err_404)]; request.method = Method::Get; request.path = String::from("/api/v1/foo/42/bar/1234"); for route in routes { match route.is_match(&request) { false => continue, true => { request.params = route.parse(&request.path); break; }, } } let foo_id: i32 = request.get("foo_id"); let bar_id: i32 = request.get("bar_id"); assert_eq!(42, foo_id); assert_eq!(1234, bar_id); } }