diff --git a/CHANGES.md b/CHANGES.md index 6c72e3e23..708498f9b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,10 @@ # Changes -## [0.7.0] - 2018-07-10 +## [0.7.0] - 2018-07-17 ### Added -* Add `.has_prefixed_route()` method to `router::RouteInfo` for route matching with prefix awareness +* Add `.has_prefixed_resource()` method to `router::ResourceInfo` for route matching with prefix awareness * Add `HttpMessage::readlines()` for reading line by line. diff --git a/src/helpers.rs b/src/helpers.rs index a14ce9ff5..400b12253 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -92,7 +92,7 @@ impl Handler for NormalizePath { // merge slashes let p = self.re_merge.replace_all(req.path(), "/"); if p.len() != req.path().len() { - if req.resource().has_prefixed_route(p.as_ref()) { + if req.resource().has_prefixed_resource(p.as_ref()) { let p = if !query.is_empty() { p + "?" + query } else { @@ -105,7 +105,7 @@ impl Handler for NormalizePath { // merge slashes and append trailing slash if self.append && !p.ends_with('/') { let p = p.as_ref().to_owned() + "/"; - if req.resource().has_prefixed_route(&p) { + if req.resource().has_prefixed_resource(&p) { let p = if !query.is_empty() { p + "?" + query } else { @@ -120,7 +120,7 @@ impl Handler for NormalizePath { // try to remove trailing slash if p.ends_with('/') { let p = p.as_ref().trim_right_matches('/'); - if req.resource().has_prefixed_route(p) { + if req.resource().has_prefixed_resource(p) { let mut req = HttpResponse::build(self.redirect); return if !query.is_empty() { req.header( @@ -135,7 +135,7 @@ impl Handler for NormalizePath { } else if p.ends_with('/') { // try to remove trailing slash let p = p.as_ref().trim_right_matches('/'); - if req.resource().has_prefixed_route(p) { + if req.resource().has_prefixed_resource(p) { let mut req = HttpResponse::build(self.redirect); return if !query.is_empty() { req.header( @@ -151,7 +151,7 @@ impl Handler for NormalizePath { // append trailing slash if self.append && !req.path().ends_with('/') { let p = req.path().to_owned() + "/"; - if req.resource().has_prefixed_route(&p) { + if req.resource().has_prefixed_resource(&p) { let p = if !query.is_empty() { p + "?" + query } else { diff --git a/src/httprequest.rs b/src/httprequest.rs index 216888777..67afaf03b 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -436,10 +436,10 @@ mod tests { router.register_resource(resource); let info = router.default_route_info(); - assert!(info.has_route("/user/test.html")); - assert!(info.has_prefixed_route("/user/test.html")); - assert!(!info.has_route("/test/unknown")); - assert!(!info.has_prefixed_route("/test/unknown")); + assert!(info.has_resource("/user/test.html")); + assert!(info.has_prefixed_resource("/user/test.html")); + assert!(!info.has_resource("/test/unknown")); + assert!(!info.has_prefixed_resource("/test/unknown")); let req = TestRequest::with_header(header::HOST, "www.rust-lang.org") .finish_with_router(router); @@ -468,10 +468,10 @@ mod tests { let mut info = router.default_route_info(); info.set_prefix(7); - assert!(info.has_route("/user/test.html")); - assert!(!info.has_prefixed_route("/user/test.html")); - assert!(!info.has_route("/prefix/user/test.html")); - assert!(info.has_prefixed_route("/prefix/user/test.html")); + assert!(info.has_resource("/user/test.html")); + assert!(!info.has_prefixed_resource("/user/test.html")); + assert!(!info.has_resource("/prefix/user/test.html")); + assert!(info.has_prefixed_resource("/prefix/user/test.html")); let req = TestRequest::with_uri("/prefix/test") .prefix(7) @@ -493,10 +493,10 @@ mod tests { let mut info = router.default_route_info(); info.set_prefix(7); - assert!(info.has_route("/index.html")); - assert!(!info.has_prefixed_route("/index.html")); - assert!(!info.has_route("/prefix/index.html")); - assert!(info.has_prefixed_route("/prefix/index.html")); + assert!(info.has_resource("/index.html")); + assert!(!info.has_prefixed_resource("/index.html")); + assert!(!info.has_resource("/prefix/index.html")); + assert!(info.has_prefixed_resource("/prefix/index.html")); let req = TestRequest::with_uri("/prefix/test") .prefix(7) @@ -518,8 +518,8 @@ mod tests { ); let info = router.default_route_info(); - assert!(!info.has_route("https://youtube.com/watch/unknown")); - assert!(!info.has_prefixed_route("https://youtube.com/watch/unknown")); + assert!(!info.has_resource("https://youtube.com/watch/unknown")); + assert!(!info.has_prefixed_resource("https://youtube.com/watch/unknown")); let req = TestRequest::default().finish_with_router(router); let url = req.url_for("youtube", &["oHg5SJYRHA0"]); diff --git a/src/router.rs b/src/router.rs index fe3ecb94d..e79dc93da 100644 --- a/src/router.rs +++ b/src/router.rs @@ -37,7 +37,7 @@ enum ResourceItem { /// Interface for application router. pub struct Router { - defs: Rc, + rmap: Rc, patterns: Vec>, resources: Vec>, default: Option>, @@ -46,7 +46,7 @@ pub struct Router { /// Information about current resource #[derive(Clone)] pub struct ResourceInfo { - router: Rc, + rmap: Rc, resource: ResourceId, params: Params, prefix: u16, @@ -57,7 +57,7 @@ impl ResourceInfo { #[inline] pub fn name(&self) -> &str { if let ResourceId::Normal(idx) = self.resource { - self.router.patterns[idx as usize].name() + self.rmap.patterns[idx as usize].0.name() } else { "" } @@ -67,7 +67,7 @@ impl ResourceInfo { #[inline] pub fn rdef(&self) -> Option<&ResourceDef> { if let ResourceId::Normal(idx) = self.resource { - Some(&self.router.patterns[idx as usize]) + Some(&self.rmap.patterns[idx as usize].0) } else { None } @@ -111,7 +111,7 @@ impl ResourceInfo { U: IntoIterator, I: AsRef, { - if let Some(pattern) = self.router.named.get(name) { + if let Some(pattern) = self.rmap.named.get(name) { let path = pattern.resource_path(elements, &req.path()[..(self.prefix as usize)])?; if path.starts_with('/') { @@ -130,24 +130,17 @@ impl ResourceInfo { } } - /// Check if application contains matching route. + /// Check if application contains matching resource. /// /// This method does not take `prefix` into account. /// For example if prefix is `/test` and router contains route `/name`, - /// following path would be recognizable `/test/name` but `has_route()` call + /// following path would be recognizable `/test/name` but `has_resource()` call /// would return `false`. - pub fn has_route(&self, path: &str) -> bool { - let path = if path.is_empty() { "/" } else { path }; - - for pattern in &self.router.patterns { - if pattern.is_match(path) { - return true; - } - } - false + pub fn has_resource(&self, path: &str) -> bool { + self.rmap.has_resource(path) } - /// Check if application contains matching route. + /// Check if application contains matching resource. /// /// This method does take `prefix` into account /// but behaves like `has_route` in case `prefix` is not set in the router. @@ -157,18 +150,35 @@ impl ResourceInfo { /// would return `true`. /// It will not match against prefix in case it's not given. For example for `/name` /// with a `/test` prefix would return `false` - pub fn has_prefixed_route(&self, path: &str) -> bool { + pub fn has_prefixed_resource(&self, path: &str) -> bool { let prefix = self.prefix as usize; if prefix >= path.len() { return false; } - self.has_route(&path[prefix..]) + self.rmap.has_resource(&path[prefix..]) } } -struct Inner { +pub(crate) struct ResourceMap { named: HashMap, - patterns: Vec, + patterns: Vec<(ResourceDef, Option>)>, +} + +impl ResourceMap { + pub fn has_resource(&self, path: &str) -> bool { + let path = if path.is_empty() { "/" } else { path }; + + for (pattern, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if let Some(plen) = pattern.is_prefix_match(path) { + return rmap.has_resource(&path[plen..]); + } + } else if pattern.is_match(path) { + return true; + } + } + false + } } impl Default for Router { @@ -180,7 +190,7 @@ impl Default for Router { impl Router { pub(crate) fn new() -> Self { Router { - defs: Rc::new(Inner { + rmap: Rc::new(ResourceMap { named: HashMap::new(), patterns: Vec::new(), }), @@ -195,7 +205,7 @@ impl Router { ResourceInfo { params, prefix: 0, - router: self.defs.clone(), + rmap: self.rmap.clone(), resource: ResourceId::Normal(idx), } } @@ -208,7 +218,7 @@ impl Router { ResourceInfo { params, prefix: 0, - router: self.defs.clone(), + rmap: self.rmap.clone(), resource: ResourceId::Default, } } @@ -217,7 +227,7 @@ impl Router { pub(crate) fn default_route_info(&self) -> ResourceInfo { ResourceInfo { params: Params::new(), - router: self.defs.clone(), + rmap: self.rmap.clone(), resource: ResourceId::Default, prefix: 0, } @@ -225,18 +235,18 @@ impl Router { pub(crate) fn register_resource(&mut self, resource: Resource) { { - let inner = Rc::get_mut(&mut self.defs).unwrap(); + let rmap = Rc::get_mut(&mut self.rmap).unwrap(); let name = resource.get_name(); if !name.is_empty() { assert!( - !inner.named.contains_key(name), + !rmap.named.contains_key(name), "Named resource {:?} is registered.", name ); - inner.named.insert(name.to_owned(), resource.rdef().clone()); + rmap.named.insert(name.to_owned(), resource.rdef().clone()); } - inner.patterns.push(resource.rdef().clone()); + rmap.patterns.push((resource.rdef().clone(), None)); } self.patterns .push(ResourcePattern::Resource(resource.rdef().clone())); @@ -244,10 +254,10 @@ impl Router { } pub(crate) fn register_scope(&mut self, mut scope: Scope) { - Rc::get_mut(&mut self.defs) + Rc::get_mut(&mut self.rmap) .unwrap() .patterns - .push(scope.rdef().clone()); + .push((scope.rdef().clone(), Some(scope.router().rmap.clone()))); let filters = scope.take_filters(); self.patterns .push(ResourcePattern::Scope(scope.rdef().clone(), filters)); @@ -259,10 +269,10 @@ impl Router { filters: Option>>>, ) { let rdef = ResourceDef::prefix(path); - Rc::get_mut(&mut self.defs) + Rc::get_mut(&mut self.rmap) .unwrap() .patterns - .push(rdef.clone()); + .push((rdef.clone(), None)); self.resources.push(ResourceItem::Handler(hnd)); self.patterns.push(ResourcePattern::Handler(rdef, filters)); } @@ -298,13 +308,13 @@ impl Router { } pub(crate) fn register_external(&mut self, name: &str, rdef: ResourceDef) { - let inner = Rc::get_mut(&mut self.defs).unwrap(); + let rmap = Rc::get_mut(&mut self.rmap).unwrap(); assert!( - !inner.named.contains_key(name), + !rmap.named.contains_key(name), "Named resource {:?} is registered.", name ); - inner.named.insert(name.to_owned(), rdef); + rmap.named.insert(name.to_owned(), rdef); } pub(crate) fn register_route(&mut self, path: &str, method: Method, f: F) @@ -406,7 +416,7 @@ impl Router { ResourceInfo { prefix: tail as u16, params: Params::new(), - router: self.defs.clone(), + rmap: self.rmap.clone(), resource: ResourceId::Default, } } @@ -534,6 +544,54 @@ impl ResourceDef { } } + fn is_prefix_match(&self, path: &str) -> Option { + let plen = path.len(); + let path = if path.is_empty() { "/" } else { path }; + + match self.tp { + PatternType::Static(ref s) => if s == path { + Some(plen) + } else { + None + }, + PatternType::Dynamic(ref re, _, len) => { + if let Some(captures) = re.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(plen + pos + len) + } else { + None + } + } + PatternType::Prefix(ref s) => { + let len = if path == s { + s.len() + } else if path.starts_with(s) + && (s.ends_with('/') || path.split_at(s.len()).1.starts_with('/')) + { + if s.ends_with('/') { + s.len() - 1 + } else { + s.len() + } + } else { + return None; + }; + Some(min(plen, len)) + } + } + } + /// Are the given path and parameters a match against this resource? pub fn match_with_params(&self, req: &Request, plen: usize) -> Option { let path = &req.path()[plen..]; @@ -588,7 +646,9 @@ impl ResourceDef { match self.tp { PatternType::Static(ref s) => if s == path { - Some(Params::with_url(req.url())) + let mut params = Params::with_url(req.url()); + params.set_tail(req.path().len() as u16); + Some(params) } else { None }, @@ -1008,4 +1068,24 @@ mod tests { assert_eq!(info.resource, ResourceId::Normal(1)); assert_eq!(info.name(), "r2"); } + + #[test] + fn test_has_resource() { + let mut router = Router::<()>::new(); + 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")); + } } diff --git a/src/scope.rs b/src/scope.rs index a12bcafa2..43d078529 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -73,6 +73,10 @@ impl Scope { &self.rdef } + pub(crate) fn router(&self) -> &Router { + self.router.as_ref() + } + #[inline] pub(crate) fn take_filters(&mut self) -> Vec>> { mem::replace(&mut self.filters, Vec::new())