diff --git a/.travis.yml b/.travis.yml index e7c20055..a9c123c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,6 +39,7 @@ script: cd actix-rt && cargo test && cd .. cd actix-connector && cargo test && cd .. cd actix-utils && cargo test && cd .. + cd router && cargo test && cd .. fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then @@ -52,4 +53,5 @@ script: cd actix-codec && cargo tarpaulin --out Xml && bash <(curl -s https://codecov.io/bash) && cd .. cd actix-server && cargo tarpaulin --out Xml && bash <(curl -s https://codecov.io/bash) && cd .. cd actix-utils && cargo tarpaulin --out Xml && bash <(curl -s https://codecov.io/bash) && cd .. + cd router && cargo tarpaulin --out Xml && bash <(curl -s https://codecov.io/bash) && cd .. fi diff --git a/Cargo.toml b/Cargo.toml index f71868dc..df052b1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "actix-server", "actix-test-server", "actix-utils", + "router", ] [dev-dependencies] diff --git a/router/Cargo.toml b/router/Cargo.toml new file mode 100644 index 00000000..44fb90a5 --- /dev/null +++ b/router/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "actix-router" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Path router" +keywords = ["actix"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-net.git" +documentation = "https://actix.rs/api/actix-net/stable/actix_router/" +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +edition = "2018" +workspace = "../" + +[lib] +name = "actix_router" +path = "src/lib.rs" + +[features] +default = ["http"] + +[dependencies] +bytes = "0.4" +regex = "1.0" +serde = "1.0.80" +string = "0.1" + +http = { version="0.1.14", optional=true } + +[dev-dependencies] +serde_derive = "1.0" diff --git a/router/src/de.rs b/router/src/de.rs new file mode 100644 index 00000000..02e6ca16 --- /dev/null +++ b/router/src/de.rs @@ -0,0 +1,611 @@ +use serde::de::{self, Deserializer, Error as DeError, Visitor}; +use serde::forward_to_deserialize_any; + +use crate::path::{Path, PathIter}; +use crate::RequestPath; + +macro_rules! unsupported_type { + ($trait_fn:ident, $name:expr) => { + fn $trait_fn(self, _: V) -> Result + where V: Visitor<'de> + { + Err(de::value::Error::custom(concat!("unsupported type: ", $name))) + } + }; +} + +macro_rules! parse_single_value { + ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + fn $trait_fn(self, visitor: V) -> Result + where V: Visitor<'de> + { + if self.path.len() != 1 { + Err(de::value::Error::custom( + format!("wrong number of parameters: {} expected 1", + self.path.len()).as_str())) + } else { + let v = self.path[0].parse().map_err( + |_| de::value::Error::custom( + format!("can not parse {:?} to a {}", &self.path[0], $tp)))?; + visitor.$visit_fn(v) + } + } + } +} + +pub struct PathDeserializer<'de, T: RequestPath + 'de> { + path: &'de Path, +} + +impl<'de, T: RequestPath + 'de> PathDeserializer<'de, T> { + pub fn new(path: &'de Path) -> Self { + PathDeserializer { path } + } +} + +impl<'de, T: RequestPath + 'de> Deserializer<'de> for PathDeserializer<'de, T> { + type Error = de::value::Error; + + fn deserialize_map(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_map(ParamsDeserializer { + params: self.path.iter(), + current: None, + }) + } + + fn deserialize_struct( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_map(visitor) + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_unit(visitor) + } + + fn deserialize_newtype_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_tuple(self, len: usize, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.len() < len { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected {}", + self.path.len(), + len + ) + .as_str(), + )) + } else { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + } + + fn deserialize_tuple_struct( + self, + _: &'static str, + len: usize, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + if self.path.len() < len { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected {}", + self.path.len(), + len + ) + .as_str(), + )) + } else { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + } + + fn deserialize_enum( + self, + _: &'static str, + _: &'static [&'static str], + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: enum")) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.len() != 1 { + Err(de::value::Error::custom( + format!("wrong number of parameters: {} expected 1", self.path.len()).as_str(), + )) + } else { + visitor.visit_str(&self.path[0]) + } + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + + unsupported_type!(deserialize_any, "'any'"); + unsupported_type!(deserialize_bytes, "bytes"); + unsupported_type!(deserialize_option, "Option"); + unsupported_type!(deserialize_identifier, "identifier"); + unsupported_type!(deserialize_ignored_any, "ignored_any"); + + parse_single_value!(deserialize_bool, visit_bool, "bool"); + parse_single_value!(deserialize_i8, visit_i8, "i8"); + parse_single_value!(deserialize_i16, visit_i16, "i16"); + parse_single_value!(deserialize_i32, visit_i32, "i32"); + parse_single_value!(deserialize_i64, visit_i64, "i64"); + parse_single_value!(deserialize_u8, visit_u8, "u8"); + parse_single_value!(deserialize_u16, visit_u16, "u16"); + parse_single_value!(deserialize_u32, visit_u32, "u32"); + parse_single_value!(deserialize_u64, visit_u64, "u64"); + parse_single_value!(deserialize_f32, visit_f32, "f32"); + parse_single_value!(deserialize_f64, visit_f64, "f64"); + parse_single_value!(deserialize_string, visit_string, "String"); + parse_single_value!(deserialize_byte_buf, visit_string, "String"); + parse_single_value!(deserialize_char, visit_char, "char"); +} + +struct ParamsDeserializer<'de, T: RequestPath> { + params: PathIter<'de, T>, + current: Option<(&'de str, &'de str)>, +} + +impl<'de, T: RequestPath> de::MapAccess<'de> for ParamsDeserializer<'de, T> { + type Error = de::value::Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + self.current = self.params.next().map(|ref item| (item.0, item.1)); + match self.current { + Some((key, _)) => Ok(Some(seed.deserialize(Key { key })?)), + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + if let Some((_, value)) = self.current.take() { + seed.deserialize(Value { value }) + } else { + Err(de::value::Error::custom("unexpected item")) + } + } +} + +struct Key<'de> { + key: &'de str, +} + +impl<'de> Deserializer<'de> for Key<'de> { + type Error = de::value::Error; + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_str(self.key) + } + + fn deserialize_any(self, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("Unexpected")) + } + + forward_to_deserialize_any! { + bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes + byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct enum ignored_any + } +} + +macro_rules! parse_value { + ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + fn $trait_fn(self, visitor: V) -> Result + where V: Visitor<'de> + { + let v = self.value.parse().map_err( + |_| de::value::Error::custom( + format!("can not parse {:?} to a {}", self.value, $tp)))?; + visitor.$visit_fn(v) + } + } +} + +struct Value<'de> { + value: &'de str, +} + +impl<'de> Deserializer<'de> for Value<'de> { + type Error = de::value::Error; + + parse_value!(deserialize_bool, visit_bool, "bool"); + parse_value!(deserialize_i8, visit_i8, "i8"); + parse_value!(deserialize_i16, visit_i16, "i16"); + parse_value!(deserialize_i32, visit_i32, "i16"); + parse_value!(deserialize_i64, visit_i64, "i64"); + parse_value!(deserialize_u8, visit_u8, "u8"); + parse_value!(deserialize_u16, visit_u16, "u16"); + parse_value!(deserialize_u32, visit_u32, "u32"); + parse_value!(deserialize_u64, visit_u64, "u64"); + parse_value!(deserialize_f32, visit_f32, "f32"); + parse_value!(deserialize_f64, visit_f64, "f64"); + parse_value!(deserialize_string, visit_string, "String"); + parse_value!(deserialize_byte_buf, visit_string, "String"); + parse_value!(deserialize_char, visit_char, "char"); + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_borrowed_bytes(self.value.as_bytes()) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_borrowed_str(self.value) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_some(self) + } + + fn deserialize_enum( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_enum(ValueEnum { value: self.value }) + } + + fn deserialize_newtype_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_tuple(self, _: usize, _: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: tuple")) + } + + fn deserialize_struct( + self, + _: &'static str, + _: &'static [&'static str], + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: struct")) + } + + fn deserialize_tuple_struct( + self, + _: &'static str, + _: usize, + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: tuple struct")) + } + + unsupported_type!(deserialize_any, "any"); + unsupported_type!(deserialize_seq, "seq"); + unsupported_type!(deserialize_map, "map"); + unsupported_type!(deserialize_identifier, "identifier"); +} + +struct ParamsSeq<'de, T: RequestPath> { + params: PathIter<'de, T>, +} + +impl<'de, T: RequestPath> de::SeqAccess<'de> for ParamsSeq<'de, T> { + type Error = de::value::Error; + + fn next_element_seed(&mut self, seed: U) -> Result, Self::Error> + where + U: de::DeserializeSeed<'de>, + { + match self.params.next() { + Some(item) => Ok(Some(seed.deserialize(Value { value: item.1 })?)), + None => Ok(None), + } + } +} + +struct ValueEnum<'de> { + value: &'de str, +} + +impl<'de> de::EnumAccess<'de> for ValueEnum<'de> { + type Error = de::value::Error; + type Variant = UnitVariant; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> + where + V: de::DeserializeSeed<'de>, + { + Ok((seed.deserialize(Key { key: self.value })?, UnitVariant)) + } +} + +struct UnitVariant; + +impl<'de> de::VariantAccess<'de> for UnitVariant { + type Error = de::value::Error; + + fn unit_variant(self) -> Result<(), Self::Error> { + Ok(()) + } + + fn newtype_variant_seed(self, _seed: T) -> Result + where + T: de::DeserializeSeed<'de>, + { + Err(de::value::Error::custom("not supported")) + } + + fn tuple_variant(self, _len: usize, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("not supported")) + } + + fn struct_variant( + self, + _: &'static [&'static str], + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("not supported")) + } +} + +#[cfg(test)] +mod tests { + use serde::de; + use serde_derive::Deserialize; + + use super::*; + use crate::path::Path; + use crate::router::Router; + + #[derive(Deserialize)] + struct MyStruct { + key: String, + value: String, + } + + #[derive(Deserialize)] + struct Id { + id: String, + } + + #[derive(Deserialize)] + struct Test2 { + key: String, + value: u32, + } + + #[test] + fn test_request_extract() { + let mut router = Router::<()>::build(); + router.path("/{key}/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/user1/"); + assert!(router.recognize(&mut path).is_some()); + + let s: MyStruct = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, "user1"); + + let s: (String, String) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, "user1"); + + let mut router = Router::<()>::build(); + router.path("/{key}/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/32/"); + assert!(router.recognize(&mut path).is_some()); + + let s: Test2 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, 32); + + let s: (String, u8) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, 32); + + let res: Vec = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(res[0], "name".to_owned()); + assert_eq!(res[1], "32".to_owned()); + } + + #[test] + fn test_extract_path_single() { + let mut router = Router::<()>::build(); + router.path("/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/32/"); + assert!(router.recognize(&mut path).is_some()); + let i: i8 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, 32); + } + + // #[test] + // fn test_extract_path_decode() { + // let mut router = Router::<()>::default(); + // router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); + + // macro_rules! test_single_value { + // ($value:expr, $expected:expr) => {{ + // let req = TestRequest::with_uri($value).finish(); + // let info = router.recognize(&req, &(), 0); + // let req = req.with_route_info(info); + // assert_eq!( + // *Path::::from_request(&req, &PathConfig::default()).unwrap(), + // $expected + // ); + // }}; + // } + + // test_single_value!("/%25/", "%"); + // test_single_value!("/%40%C2%A3%24%25%5E%26%2B%3D/", "@£$%^&+="); + // test_single_value!("/%2B/", "+"); + // test_single_value!("/%252B/", "%2B"); + // test_single_value!("/%2F/", "/"); + // test_single_value!("/%252F/", "%2F"); + // test_single_value!( + // "/http%3A%2F%2Flocalhost%3A80%2Ffoo/", + // "http://localhost:80/foo" + // ); + // test_single_value!("/%2Fvar%2Flog%2Fsyslog/", "/var/log/syslog"); + // test_single_value!( + // "/http%3A%2F%2Flocalhost%3A80%2Ffile%2F%252Fvar%252Flog%252Fsyslog/", + // "http://localhost:80/file/%2Fvar%2Flog%2Fsyslog" + // ); + + // let req = TestRequest::with_uri("/%25/7/?id=test").finish(); + + // let mut router = Router::<()>::default(); + // router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); + // let info = router.recognize(&req, &(), 0); + // let req = req.with_route_info(info); + + // let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); + // assert_eq!(s.key, "%"); + // assert_eq!(s.value, 7); + + // let s = Path::<(String, String)>::from_request(&req, &PathConfig::default()).unwrap(); + // assert_eq!(s.0, "%"); + // assert_eq!(s.1, "7"); + // } + + // #[test] + // fn test_extract_path_no_decode() { + // let mut router = Router::<()>::default(); + // router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); + + // let req = TestRequest::with_uri("/%25/").finish(); + // let info = router.recognize(&req, &(), 0); + // let req = req.with_route_info(info); + // assert_eq!( + // *Path::::from_request(&req, &&PathConfig::default().disable_decoding()) + // .unwrap(), + // "%25" + // ); + // } +} diff --git a/router/src/lib.rs b/router/src/lib.rs new file mode 100644 index 00000000..c2135055 --- /dev/null +++ b/router/src/lib.rs @@ -0,0 +1,32 @@ +//! Resource path matching library. +mod de; +mod path; +mod pattern; +mod router; + +pub use self::de::PathDeserializer; +pub use self::path::Path; +pub use self::pattern::Pattern; +pub use self::router::{Router, RouterBuilder}; + +pub trait RequestPath { + fn path(&self) -> &str; +} + +impl RequestPath for String { + fn path(&self) -> &str { + self.as_str() + } +} + +impl<'a> RequestPath for &'a str { + fn path(&self) -> &str { + self + } +} + +impl> RequestPath for string::String { + fn path(&self) -> &str { + &*self + } +} diff --git a/router/src/path.rs b/router/src/path.rs new file mode 100644 index 00000000..0087dfdd --- /dev/null +++ b/router/src/path.rs @@ -0,0 +1,173 @@ +use std::ops::Index; +use std::rc::Rc; + +use crate::RequestPath; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum PathItem { + Static(&'static str), + Segment(u16, u16), +} + +/// Resource path match information +/// +/// If resource path contains variable patterns, `Path` stores them. +#[derive(Debug, Clone)] +pub struct Path { + path: T, + pub(crate) skip: u16, + pub(crate) segments: Vec<(Rc, PathItem)>, +} + +impl Default for Path { + fn default() -> Self { + Path { + path: T::default(), + skip: 0, + segments: Vec::new(), + } + } +} + +impl Path { + pub fn new(path: T) -> Path { + Path { + path, + skip: 0, + segments: Vec::new(), + } + } + + pub fn path(&self) -> &str { + let skip = self.skip as usize; + let path = self.path.path(); + if skip <= path.len() { + &path[skip..] + } else { + "" + } + } + + pub fn set_path(&mut self, path: T) { + self.skip = 0; + self.path = path; + self.segments.clear(); + } + + /// Skip first `n` chars in path + pub fn skip(&mut self, n: u16) { + self.skip = self.skip + n; + } + + pub(crate) fn add(&mut self, name: Rc, value: PathItem) { + match value { + PathItem::Static(s) => self.segments.push((name, PathItem::Static(s))), + PathItem::Segment(begin, end) => self + .segments + .push((name, PathItem::Segment(self.skip + begin, self.skip + end))), + } + } + + pub(crate) fn add_static(&mut self, name: &str, value: &'static str) { + self.segments + .push((Rc::new(name.to_string()), PathItem::Static(value))); + } + + /// Check if there are any matched patterns + pub fn is_empty(&self) -> bool { + self.segments.is_empty() + } + + /// Check number of extracted parameters + pub fn len(&self) -> usize { + self.segments.len() + } + + /// Get matched parameter by name without type conversion + pub fn get(&self, key: &str) -> Option<&str> { + for item in self.segments.iter() { + if key == item.0.as_str() { + return match item.1 { + PathItem::Static(ref s) => Some(&s), + PathItem::Segment(s, e) => { + Some(&self.path.path()[(s as usize)..(e as usize)]) + } + }; + } + } + if key == "tail" { + Some(&self.path.path()[(self.skip as usize)..]) + } else { + None + } + } + + /// Get unprocessed part of path + pub fn unprocessed(&self) -> &str { + &self.path.path()[(self.skip as usize)..] + } + + /// Get matched parameter by name. + /// + /// If keyed parameter is not available empty string is used as default + /// value. + pub fn query(&self, key: &str) -> &str { + if let Some(s) = self.get(key) { + s + } else { + "" + } + } + + /// Return iterator to items in parameter container + pub fn iter(&self) -> PathIter { + PathIter { + idx: 0, + params: self, + } + } +} + +#[derive(Debug)] +pub struct PathIter<'a, T> { + idx: usize, + params: &'a Path, +} + +impl<'a, T: RequestPath> Iterator for PathIter<'a, T> { + type Item = (&'a str, &'a str); + + #[inline] + fn next(&mut self) -> Option<(&'a str, &'a str)> { + if self.idx < self.params.len() { + let idx = self.idx; + let res = match self.params.segments[idx].1 { + PathItem::Static(ref s) => &s, + PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], + }; + self.idx += 1; + return Some((&self.params.segments[idx].0, res)); + } + None + } +} + +impl<'a, T: RequestPath> Index<&'a str> for Path { + type Output = str; + + fn index(&self, name: &'a str) -> &str { + self.get(name) + .expect("Value for parameter is not available") + } +} + +impl Index for Path { + type Output = str; + + fn index(&self, idx: usize) -> &str { + match self.segments[idx].1 { + PathItem::Static(ref s) => &s, + PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], + } + } +} diff --git a/router/src/pattern.rs b/router/src/pattern.rs new file mode 100644 index 00000000..d589700a --- /dev/null +++ b/router/src/pattern.rs @@ -0,0 +1,373 @@ +use std::cmp::min; +use std::hash::{Hash, Hasher}; +use std::rc::Rc; + +use regex::{escape, Regex}; + +use crate::path::{Path, PathItem}; +use crate::RequestPath; + +const MAX_DYNAMIC_SEGMENTS: usize = 16; + +/// Resource type describes an entry in resources table +/// +/// Resource pattern can contain only 16 dynamic segments +#[derive(Clone, Debug)] +pub struct Pattern { + tp: PatternType, + pattern: String, + elements: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +enum PatternElement { + Str(String), + Var(String), +} + +#[derive(Clone, Debug)] +enum PatternType { + Static(String), + Prefix(String), + Dynamic(Regex, Vec>, usize), +} + +impl Pattern { + /// Parse path pattern and create new `Pattern` instance. + /// + /// Panics if path pattern is wrong. + pub fn new(path: &str) -> Self { + Pattern::with_prefix(path, false) + } + + /// Parse path pattern and create new `Pattern` instance. + /// + /// Use `prefix` type instead of `static`. + /// + /// Panics if path regex pattern is wrong. + pub fn prefix(path: &str) -> Self { + Pattern::with_prefix(path, true) + } + + /// Parse path pattern and create new `Pattern` instance with custom prefix + fn with_prefix(path: &str, for_prefix: bool) -> Self { + let path = path.to_owned(); + let (pattern, elements, is_dynamic, len) = Pattern::parse(&path, for_prefix); + + let tp = if is_dynamic { + let re = match Regex::new(&pattern) { + Ok(re) => re, + Err(err) => panic!("Wrong path pattern: \"{}\" {}", path, err), + }; + // actix creates one router per thread + let names = re + .capture_names() + .filter_map(|name| name.map(|name| Rc::new(name.to_owned()))) + .collect(); + PatternType::Dynamic(re, names, len) + } else if for_prefix { + PatternType::Prefix(pattern.clone()) + } else { + PatternType::Static(pattern.clone()) + }; + + Pattern { + tp, + elements, + pattern: path.to_owned(), + } + } + + /// Path pattern of the resource + pub fn pattern(&self) -> &str { + &self.pattern + } + + /// Check if path matchs this pattern? + pub fn is_match(&self, path: &str) -> bool { + match self.tp { + PatternType::Static(ref s) => s == path, + PatternType::Dynamic(ref re, _, _) => re.is_match(path), + PatternType::Prefix(ref s) => path.starts_with(s), + } + } + + /// Is the given path and parameters a match against this pattern? + pub fn match_path(&self, path: &mut Path) -> bool { + match self.tp { + PatternType::Static(ref s) => { + if s == path.path() { + path.skip(path.len() as u16); + true + } else { + false + } + } + PatternType::Dynamic(ref re, ref names, len) => { + let mut idx = 0; + let mut pos = 0; + let mut segments: [PathItem; MAX_DYNAMIC_SEGMENTS] = + [PathItem::Static(""); MAX_DYNAMIC_SEGMENTS]; + + if let Some(captures) = re.captures(path.path()) { + let mut passed = false; + + for capture in captures.iter() { + if let Some(ref m) = capture { + if !passed { + passed = true; + continue; + } + + segments[idx] = PathItem::Segment(m.start() as u16, m.end() as u16); + idx += 1; + pos = m.end(); + } + } + } else { + return false; + } + for idx in 0..idx { + path.add(names[idx].clone(), segments[idx]); + } + path.skip((pos + len) as u16); + true + } + PatternType::Prefix(ref s) => { + let rpath = path.path(); + let len = if s == rpath { + s.len() + } else if rpath.starts_with(s) + && (s.ends_with('/') || rpath.split_at(s.len()).1.starts_with('/')) + { + if s.ends_with('/') { + s.len() - 1 + } else { + s.len() + } + } else { + return false; + }; + path.skip(min(rpath.len(), len) as u16); + true + } + } + } + + // /// Build resource path. + // pub fn resource_path( + // &self, path: &mut String, elements: &mut U, + // ) -> Result<(), UrlGenerationError> + // where + // U: Iterator, + // I: AsRef, + // { + // match self.tp { + // PatternType::Prefix(ref p) => path.push_str(p), + // PatternType::Static(ref p) => path.push_str(p), + // PatternType::Dynamic(..) => { + // for el in &self.elements { + // match *el { + // PatternElement::Str(ref s) => path.push_str(s), + // PatternElement::Var(_) => { + // if let Some(val) = elements.next() { + // path.push_str(val.as_ref()) + // } else { + // return Err(UrlGenerationError::NotEnoughElements); + // } + // } + // } + // } + // } + // }; + // Ok(()) + // } + + fn parse_param(pattern: &str) -> (PatternElement, String, &str) { + const DEFAULT_PATTERN: &str = "[^/]+"; + let mut params_nesting = 0usize; + let close_idx = pattern + .find(|c| match c { + '{' => { + params_nesting += 1; + false + } + '}' => { + params_nesting -= 1; + params_nesting == 0 + } + _ => false, + }) + .expect("malformed dynamic segment"); + let (mut param, rem) = pattern.split_at(close_idx + 1); + param = ¶m[1..param.len() - 1]; // Remove outer brackets + let (name, pattern) = match param.find(':') { + Some(idx) => { + let (name, pattern) = param.split_at(idx); + (name, &pattern[1..]) + } + None => (param, DEFAULT_PATTERN), + }; + ( + PatternElement::Var(name.to_string()), + format!(r"(?P<{}>{})", &name, &pattern), + rem, + ) + } + + fn parse( + mut pattern: &str, + for_prefix: bool, + ) -> (String, Vec, bool, usize) { + if pattern.find('{').is_none() { + return ( + String::from(pattern), + vec![PatternElement::Str(String::from(pattern))], + false, + pattern.chars().count(), + ); + }; + + let mut elems = Vec::new(); + let mut re = String::from("^"); + let mut dyn_elems = 0; + + while let Some(idx) = pattern.find('{') { + let (prefix, rem) = pattern.split_at(idx); + elems.push(PatternElement::Str(String::from(prefix))); + re.push_str(&escape(prefix)); + let (param_pattern, re_part, rem) = Self::parse_param(rem); + elems.push(param_pattern); + re.push_str(&re_part); + pattern = rem; + dyn_elems += 1; + } + + elems.push(PatternElement::Str(String::from(pattern))); + re.push_str(&escape(pattern)); + + if dyn_elems > MAX_DYNAMIC_SEGMENTS { + panic!( + "Only {} dynanic segments are allowed, provided: {}", + MAX_DYNAMIC_SEGMENTS, dyn_elems + ); + } + + if !for_prefix { + re.push_str("$"); + } + + (re, elems, true, pattern.chars().count()) + } +} + +impl PartialEq for Pattern { + fn eq(&self, other: &Pattern) -> bool { + self.pattern == other.pattern + } +} + +impl Eq for Pattern {} + +impl Hash for Pattern { + fn hash(&self, state: &mut H) { + self.pattern.hash(state); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_static() { + let re = Pattern::new("/"); + assert!(re.is_match("/")); + assert!(!re.is_match("/a")); + + let re = Pattern::new("/name"); + assert!(re.is_match("/name")); + assert!(!re.is_match("/name1")); + assert!(!re.is_match("/name/")); + assert!(!re.is_match("/name~")); + + let re = Pattern::new("/name/"); + assert!(re.is_match("/name/")); + assert!(!re.is_match("/name")); + assert!(!re.is_match("/name/gs")); + + let re = Pattern::new("/user/profile"); + assert!(re.is_match("/user/profile")); + assert!(!re.is_match("/user/profile/profile")); + } + + #[test] + fn test_parse_param() { + let re = Pattern::new("/user/{id}"); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let mut path = Path::new("/user/profile"); + assert!(re.match_path(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + + let mut path = Path::new("/user/1245125"); + assert!(re.match_path(&mut path)); + assert_eq!(path.get("id").unwrap(), "1245125"); + + let re = Pattern::new("/v{version}/resource/{id}"); + assert!(re.is_match("/v1/resource/320120")); + assert!(!re.is_match("/v/resource/1")); + assert!(!re.is_match("/resource")); + + let mut path = Path::new("/v151/resource/adahg32"); + assert!(re.match_path(&mut path)); + assert_eq!(path.get("version").unwrap(), "151"); + assert_eq!(path.get("id").unwrap(), "adahg32"); + + let re = Pattern::new("/{id:[[:digit:]]{6}}"); + assert!(re.is_match("/012345")); + assert!(!re.is_match("/012")); + assert!(!re.is_match("/01234567")); + assert!(!re.is_match("/XXXXXX")); + + let mut path = Path::new("/012345"); + assert!(re.match_path(&mut path)); + assert_eq!(path.get("id").unwrap(), "012345"); + } + + #[test] + fn test_resource_prefix() { + let re = Pattern::prefix("/name"); + assert!(re.is_match("/name")); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/test/test")); + assert!(re.is_match("/name1")); + assert!(re.is_match("/name~")); + + let re = Pattern::prefix("/name/"); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + } + + #[test] + fn test_reousrce_prefix_dynamic() { + let re = Pattern::prefix("/{name}/"); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + + let mut path = Path::new("/test2/"); + assert!(re.match_path(&mut path)); + assert_eq!(&path["name"], "test2"); + assert_eq!(&path[0], "test2"); + + let mut path = Path::new("/test2/subpath1/subpath2/index.html"); + assert!(re.match_path(&mut path)); + assert_eq!(&path["name"], "test2"); + assert_eq!(&path[0], "test2"); + } +} diff --git a/router/src/router.rs b/router/src/router.rs new file mode 100644 index 00000000..a875da6e --- /dev/null +++ b/router/src/router.rs @@ -0,0 +1,387 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use crate::path::Path; +use crate::pattern::Pattern; +use crate::RequestPath; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub(crate) enum ResourceId { + Default, + Normal(u16), +} + +/// Information about current resource +#[derive(Clone, Debug)] +pub struct ResourceInfo { + rmap: Rc, + resource: ResourceId, +} + +#[derive(Default, Debug)] +pub(crate) struct ResourceMap { + root: Option, + named: HashMap, + patterns: Vec, +} + +/// Resource router. +pub struct Router { + rmap: Rc, + named: HashMap, + resources: Vec, +} + +impl Router { + pub fn build() -> RouterBuilder { + RouterBuilder { + rmap: ResourceMap::default(), + named: HashMap::new(), + resources: Vec::new(), + } + } + + pub fn recognize(&self, path: &mut Path) -> Option<(&T, ResourceInfo)> { + if !path.path().is_empty() { + for (idx, resource) in self.rmap.patterns.iter().enumerate() { + if resource.match_path(path) { + let info = ResourceInfo { + rmap: self.rmap.clone(), + resource: ResourceId::Normal(idx as u16), + }; + return Some((&self.resources[idx], info)); + } + } + } + None + } + + pub fn recognize_mut( + &mut self, + path: &mut Path, + ) -> Option<(&mut T, ResourceInfo)> { + if !path.path().is_empty() { + for (idx, resource) in self.rmap.patterns.iter().enumerate() { + if resource.match_path(path) { + let info = ResourceInfo { + rmap: self.rmap.clone(), + resource: ResourceId::Normal(idx as u16), + }; + return Some((&mut self.resources[idx], info)); + } + } + } + None + } +} + +impl ResourceMap { + fn register(&mut self, pattern: Pattern) { + self.patterns.push(pattern); + } + + fn register_named(&mut self, name: String, pattern: Pattern) { + self.patterns.push(pattern.clone()); + self.named.insert(name, pattern); + } + + fn has_resource(&self, path: &str) -> bool { + unimplemented!() + } +} + +pub struct RouterBuilder { + rmap: ResourceMap, + named: HashMap, + resources: Vec, +} + +impl RouterBuilder { + pub fn path(&mut self, path: &str, resource: T) { + self.rmap.register(Pattern::new(path)); + self.resources.push(resource); + } + + pub fn prefix(&mut self, prefix: &str, resource: T) { + self.rmap.register(Pattern::prefix(prefix)); + self.resources.push(resource); + } + + pub fn finish(self) -> Router { + Router { + rmap: Rc::new(self.rmap), + named: self.named, + resources: self.resources, + } + } +} + +#[cfg(test)] +mod tests { + use crate::path::Path; + use crate::router::{ResourceId, Router}; + + #[test] + fn test_recognizer_1() { + let mut router = Router::::build(); + router.path("/name", 10); + router.path("/name/{val}", 11); + router.path("/name/{val}/index.html", 12); + router.path("/file/{file}.{ext}", 13); + router.path("/v{val}/{val2}/index.html", 14); + router.path("/v/{tail:.*}", 15); + router.path("/test2/{test}.html", 16); + router.path("/{test}/index.html", 17); + let mut router = router.finish(); + + let mut path = Path::new("/unknown"); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/name"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + assert_eq!(info.resource, ResourceId::Normal(0)); + assert!(path.is_empty()); + + let mut path = Path::new("/name/value"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(info.resource, ResourceId::Normal(1)); + assert_eq!(path.get("val").unwrap(), "value"); + assert_eq!(&path["val"], "value"); + + let mut path = Path::new("/name/value2/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 12); + assert_eq!(info.resource, ResourceId::Normal(2)); + assert_eq!(path.get("val").unwrap(), "value2"); + + let mut path = Path::new("/file/file.gz"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 13); + assert_eq!(info.resource, ResourceId::Normal(3)); + assert_eq!(path.get("file").unwrap(), "file"); + assert_eq!(path.get("ext").unwrap(), "gz"); + + let mut path = Path::new("/vtest/ttt/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 14); + assert_eq!(info.resource, ResourceId::Normal(4)); + assert_eq!(path.get("val").unwrap(), "test"); + assert_eq!(path.get("val2").unwrap(), "ttt"); + + let mut path = Path::new("/v/blah-blah/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 15); + assert_eq!(info.resource, ResourceId::Normal(5)); + assert_eq!(path.get("tail").unwrap(), "blah-blah/index.html"); + + let mut path = Path::new("/test2/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 16); + assert_eq!(info.resource, ResourceId::Normal(6)); + assert_eq!(path.get("test").unwrap(), "index"); + + let mut path = Path::new("/bbb/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 17); + assert_eq!(info.resource, ResourceId::Normal(7)); + assert_eq!(path.get("test").unwrap(), "bbb"); + } + + #[test] + fn test_recognizer_2() { + let mut router = Router::::build(); + router.path("/index.json", 10); + router.path("/{source}.json", 11); + let mut router = router.finish(); + + let mut path = Path::new("/index.json"); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test.json"); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + } + + #[test] + fn test_recognizer_with_prefix() { + let mut router = Router::::build(); + router.path("/name", 10); + router.path("/name/{val}", 11); + let mut router = router.finish(); + + let mut path = Path::new("/name"); + path.skip(5); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test/name"); + path.skip(5); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test/name/value"); + path.skip(5); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(info.resource, ResourceId::Normal(1)); + assert_eq!(path.get("val").unwrap(), "value"); + assert_eq!(&path["val"], "value"); + + // same patterns + let mut router = Router::::build(); + router.path("/name", 10); + router.path("/name/{val}", 11); + let mut router = router.finish(); + + let mut path = Path::new("/name"); + path.skip(6); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test2/name"); + path.skip(6); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test2/name-test"); + path.skip(6); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test2/name/ttt"); + path.skip(6); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(&path["val"], "ttt"); + } + + // #[test] + // fn test_request_resource() { + // let mut router = Router::<()>::default(); + // let mut resource = Resource::new(ResourcePattern::new("/index.json")); + // resource.name("r1"); + // router.register_resource(resource); + // let mut resource = Resource::new(ResourcePattern::new("/test.json")); + // resource.name("r2"); + // router.register_resource(resource); + + // let req = TestRequest::with_uri("/index.json").finish(); + // let info = router.recognize(&req, &(), 0); + // assert_eq!(info.resource, ResourceId::Normal(0)); + + // assert_eq!(info.name(), "r1"); + + // let req = TestRequest::with_uri("/test.json").finish(); + // let info = router.recognize(&req, &(), 0); + // assert_eq!(info.resource, ResourceId::Normal(1)); + // assert_eq!(info.name(), "r2"); + // } + + // #[test] + // fn test_has_resource() { + // let mut router = Router::<()>::default(); + // let scope = Scope::new("/test").resource("/name", |_| "done"); + // router.register_scope(scope); + + // { + // let info = router.default_route_info(); + // assert!(!info.has_resource("/test")); + // assert!(info.has_resource("/test/name")); + // } + + // let scope = Scope::new("/test2").nested("/test10", |s| s.resource("/name", |_| "done")); + // router.register_scope(scope); + + // let info = router.default_route_info(); + // assert!(info.has_resource("/test2/test10/name")); + // } + + // #[test] + // fn test_url_for() { + // let mut router = Router::<()>::new(ResourcePattern::prefix("")); + + // let mut resource = Resource::new(ResourcePattern::new("/tttt")); + // resource.name("r0"); + // router.register_resource(resource); + + // let scope = Scope::new("/test").resource("/name", |r| { + // r.name("r1"); + // }); + // router.register_scope(scope); + + // let scope = + // Scope::new("/test2").nested("/test10", |s| s.resource("/name", |r| r.name("r2"))); + // router.register_scope(scope); + // router.finish(); + + // let req = TestRequest::with_uri("/test").request(); + // { + // let info = router.default_route_info(); + + // let res = info + // .url_for(&req, "r0", Vec::<&'static str>::new()) + // .unwrap(); + // assert_eq!(res.as_str(), "http://localhost:8080/tttt"); + + // let res = info + // .url_for(&req, "r1", Vec::<&'static str>::new()) + // .unwrap(); + // assert_eq!(res.as_str(), "http://localhost:8080/test/name"); + + // let res = info + // .url_for(&req, "r2", Vec::<&'static str>::new()) + // .unwrap(); + // assert_eq!(res.as_str(), "http://localhost:8080/test2/test10/name"); + // } + + // let req = TestRequest::with_uri("/test/name").request(); + // let info = router.recognize(&req, &(), 0); + // assert_eq!(info.resource, ResourceId::Normal(1)); + + // let res = info + // .url_for(&req, "r0", Vec::<&'static str>::new()) + // .unwrap(); + // assert_eq!(res.as_str(), "http://localhost:8080/tttt"); + + // let res = info + // .url_for(&req, "r1", Vec::<&'static str>::new()) + // .unwrap(); + // assert_eq!(res.as_str(), "http://localhost:8080/test/name"); + + // let res = info + // .url_for(&req, "r2", Vec::<&'static str>::new()) + // .unwrap(); + // assert_eq!(res.as_str(), "http://localhost:8080/test2/test10/name"); + // } + + // #[test] + // fn test_url_for_dynamic() { + // let mut router = Router::<()>::new(ResourcePattern::prefix("")); + + // let mut resource = Resource::new(ResourcePattern::new("/{name}/test/index.{ext}")); + // resource.name("r0"); + // router.register_resource(resource); + + // let scope = Scope::new("/{name1}").nested("/{name2}", |s| { + // s.resource("/{name3}/test/index.{ext}", |r| r.name("r2")) + // }); + // router.register_scope(scope); + // router.finish(); + + // let req = TestRequest::with_uri("/test").request(); + // { + // let info = router.default_route_info(); + + // let res = info.url_for(&req, "r0", vec!["sec1", "html"]).unwrap(); + // assert_eq!(res.as_str(), "http://localhost:8080/sec1/test/index.html"); + + // let res = info + // .url_for(&req, "r2", vec!["sec1", "sec2", "sec3", "html"]) + // .unwrap(); + // assert_eq!( + // res.as_str(), + // "http://localhost:8080/sec1/sec2/sec3/test/index.html" + // ); + // } + // } +}