From 0fe8038d2308c9057853163d5a79c685650b8c68 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 25 Dec 2019 15:10:01 +0400 Subject: [PATCH] allow specify set of resource patters --- router/CHANGES.txt | 4 + router/Cargo.toml | 8 +- router/src/resource.rs | 260 ++++++++++++++++++++++++++++++++++------- 3 files changed, 228 insertions(+), 44 deletions(-) diff --git a/router/CHANGES.txt b/router/CHANGES.txt index 6538a0b1..de6d6854 100644 --- a/router/CHANGES.txt +++ b/router/CHANGES.txt @@ -1,5 +1,9 @@ # Changes +## [0.2.1] - 2019-12-xx + +* Add multi-pattern resources + ## [0.2.0] - 2019-12-07 * Update http to 0.2 diff --git a/router/Cargo.toml b/router/Cargo.toml index b3568701..c93b0ee1 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-router" -version = "0.2.0" +version = "0.2.1" authors = ["Nikolay Kim "] description = "Path router" keywords = ["actix"] @@ -8,9 +8,7 @@ homepage = "https://actix.rs" repository = "https://github.com/actix/actix-net.git" documentation = "https://docs.rs/actix-router/" license = "MIT/Apache-2.0" -exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] edition = "2018" -workspace = ".." [lib] name = "actix_router" @@ -21,8 +19,8 @@ default = ["http"] [dependencies] regex = "1.3.1" -serde = "1.0.80" -bytestring = "0.1.0" +serde = "1.0.104" +bytestring = "0.1.2" log = "0.4.8" http = { version="0.2.0", optional=true } diff --git a/router/src/resource.rs b/router/src/resource.rs index 2f6437a4..9f18bb86 100644 --- a/router/src/resource.rs +++ b/router/src/resource.rs @@ -2,7 +2,7 @@ use std::cmp::min; use std::hash::{Hash, Hasher}; use std::rc::Rc; -use regex::{escape, Regex}; +use regex::{escape, Regex, RegexSet}; use crate::path::{Path, PathItem}; use crate::{Resource, ResourcePath}; @@ -32,21 +32,56 @@ enum PatternType { Static(String), Prefix(String), Dynamic(Regex, Vec>, usize), + DynamicSet(RegexSet, Vec<(Regex, Vec>, usize)>), } impl ResourceDef { /// Parse path pattern and create new `Pattern` instance. /// - /// Panics if path pattern is wrong. + /// Panics if path pattern is malformed. pub fn new(path: &str) -> Self { ResourceDef::with_prefix(path, false) } + /// Parse path pattern and create new `Pattern` instance. + /// + /// Panics if any of paths pattern is malformed. Every set element + /// must contain same set of capture elements. + pub fn new_set, U: AsRef>(set: T) -> Self { + let mut data = Vec::new(); + let mut re_set = Vec::new(); + + for item in set { + let path = item.as_ref().to_owned(); + let (pattern, _, _, len) = ResourceDef::parse(&path, false); + + 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: Vec<_> = re + .capture_names() + .filter_map(|name| name.map(|name| Rc::new(name.to_owned()))) + .collect(); + data.push((re, names, len)); + re_set.push(pattern); + } + + ResourceDef { + id: 0, + tp: PatternType::DynamicSet(RegexSet::new(re_set).unwrap(), data), + elements: Vec::new(), + name: String::new(), + pattern: "".to_owned(), + } + } + /// Parse path pattern and create new `Pattern` instance. /// /// Use `prefix` type instead of `static`. /// - /// Panics if path regex pattern is wrong. + /// Panics if path regex pattern is malformed. pub fn prefix(path: &str) -> Self { ResourceDef::with_prefix(path, true) } @@ -57,7 +92,7 @@ impl ResourceDef { /// /// Use `prefix` type instead of `static`. /// - /// Panics if path regex pattern is wrong. + /// Panics if path regex pattern is malformed. pub fn root_prefix(path: &str) -> Self { ResourceDef::with_prefix(&insert_slash(path), true) } @@ -123,8 +158,9 @@ impl ResourceDef { 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), + PatternType::Dynamic(ref re, _, _) => re.is_match(path), + PatternType::DynamicSet(ref re, _) => re.is_match(path), } } @@ -176,6 +212,30 @@ impl ResourceDef { }; Some(min(plen, len)) } + PatternType::DynamicSet(ref re, ref params) => { + if let Some(idx) = re.matches(path).into_iter().next() { + let (ref pattern, _, len) = params[idx]; + if let Some(captures) = pattern.captures(path) { + let mut pos = 0; + let mut passed = false; + for capture in captures.iter() { + if let Some(ref m) = capture { + if !passed { + passed = true; + continue; + } + + pos = m.end(); + } + } + Some(pos + len) + } else { + None + } + } else { + None + } + } } } @@ -190,6 +250,25 @@ impl ResourceDef { false } } + 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; + }; + let rpath_len = rpath.len(); + path.skip(min(rpath_len, len) as u16); + true + } PatternType::Dynamic(ref re, ref names, len) => { let mut idx = 0; let mut pos = 0; @@ -219,24 +298,40 @@ impl ResourceDef { 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 + PatternType::DynamicSet(ref re, ref params) => { + if let Some(idx) = re.matches(path.path()).into_iter().next() { + let (ref pattern, ref names, len) = params[idx]; + let mut idx = 0; + let mut pos = 0; + let mut segments: [PathItem; MAX_DYNAMIC_SEGMENTS] = + [PathItem::Static(""); MAX_DYNAMIC_SEGMENTS]; + + if let Some(captures) = pattern.captures(path.path()) { + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(&name) { + idx += 1; + pos = m.end(); + segments[no] = + PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + log::error!( + "Dynamic path match but not all segments found: {}", + name + ); + return false; + } + } } else { - s.len() + return false; } + for idx in 0..idx { + path.add(names[idx].clone(), segments[idx]); + } + path.skip((pos + len) as u16); + true } else { - return false; - }; - let rpath_len = rpath.len(); - path.skip(min(rpath_len, len) as u16); - true + false + } } } } @@ -263,6 +358,30 @@ impl ResourceDef { false } } + PatternType::Prefix(ref s) => { + let len = { + let rpath = res.resource_path().path(); + 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; + } + }; + if !check(res, user_data) { + return false; + } + let path = res.resource_path(); + path.skip(min(path.path().len(), len) as u16); + true + } PatternType::Dynamic(ref re, ref names, len) => { let mut idx = 0; let mut pos = 0; @@ -298,29 +417,47 @@ impl ResourceDef { path.skip((pos + len) as u16); true } - PatternType::Prefix(ref s) => { - let len = { - let rpath = res.resource_path().path(); - 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() + PatternType::DynamicSet(ref re, ref params) => { + let path = res.resource_path().path(); + if let Some(idx) = re.matches(path).into_iter().next() { + let (ref pattern, ref names, len) = params[idx]; + let mut idx = 0; + let mut pos = 0; + let mut segments: [PathItem; MAX_DYNAMIC_SEGMENTS] = + [PathItem::Static(""); MAX_DYNAMIC_SEGMENTS]; + + if let Some(captures) = pattern.captures(path) { + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(&name) { + idx += 1; + pos = m.end(); + segments[no] = + PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + log::error!( + "Dynamic path match but not all segments found: {}", + name + ); + return false; + } } } else { return false; } - }; - if !check(res, user_data) { - return false; + + if !check(res, user_data) { + return false; + } + + let path = res.resource_path(); + for idx in 0..idx { + path.add(names[idx].clone(), segments[idx]); + } + path.skip((pos + len) as u16); + true + } else { + false } - let path = res.resource_path(); - path.skip(min(path.path().len(), len) as u16); - true } } } @@ -348,7 +485,10 @@ impl ResourceDef { } } } - }; + PatternType::DynamicSet(..) => { + return false; + } + } true } @@ -558,6 +698,48 @@ mod tests { assert_eq!(path.get("id").unwrap(), "012345"); } + #[test] + fn test_dynamic_set() { + let re = ResourceDef::new_set( + vec![ + "/user/{id}", + "/v{version}/resource/{id}", + "/{id:[[:digit:]]{6}}", + ] + .iter(), + ); + 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"); + + 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"); + + 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_parse_tail() { let re = ResourceDef::new("/user/-{id}*");