diff --git a/CHANGES.md b/CHANGES.md index 22a389d10..f4c04cd05 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ * Re-export `actix_rt::main` as `actix_web::main`. * `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched resource pattern. +* `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name. ### Changed diff --git a/src/request.rs b/src/request.rs index 8ca897442..85f409016 100644 --- a/src/request.rs +++ b/src/request.rs @@ -137,6 +137,14 @@ impl HttpRequest { self.0.rmap.match_pattern(self.path()) } + /// The resource name that matched the path. Useful for logging and metrics. + /// + /// Returns a None when no resource is fully matched, including default services. + #[inline] + pub fn match_name(&self) -> Option<&str> { + self.0.rmap.match_name(self.path()) + } + /// Request extensions #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { @@ -462,6 +470,24 @@ mod tests { ); } + #[test] + fn test_match_name() { + let mut rdef = ResourceDef::new("/index.html"); + *rdef.name_mut() = "index".to_string(); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + rmap.add(&mut rdef, None); + + assert!(rmap.has_resource("/index.html")); + + let req = TestRequest::default() + .uri("/index.html") + .rmap(rmap) + .to_http_request(); + + assert_eq!(req.match_name(), Some("index")); + } + #[test] fn test_url_for_external() { let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}"); diff --git a/src/rmap.rs b/src/rmap.rs index 0a0c96777..5e79830ec 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -93,6 +93,27 @@ impl ResourceMap { false } + /// Returns the name of the route that matches the given path or None if no full match + /// is possible. + pub fn match_name(&self, path: &str) -> Option<&str> { + 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.match_name(&path[plen..]); + } + } else if pattern.is_match(path) { + return match pattern.name() { + "" => None, + s => Some(s), + }; + } + } + + None + } + /// Returns the full resource pattern matched against a path or None if no full match /// is possible. pub fn match_pattern(&self, path: &str) -> Option { @@ -302,4 +323,48 @@ mod tests { Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned()) ); } + + #[test] + fn extract_matched_name() { + let mut root = ResourceMap::new(ResourceDef::root_prefix("")); + + let mut rdef = ResourceDef::new("/info"); + *rdef.name_mut() = "root_info".to_owned(); + root.add(&mut rdef, None); + + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut rdef = ResourceDef::new("/"); + user_map.add(&mut rdef, None); + + let mut rdef = ResourceDef::new("/post/{post_id}"); + *rdef.name_mut() = "user_post".to_owned(); + user_map.add(&mut rdef, None); + + root.add( + &mut ResourceDef::root_prefix("/user/{id}"), + Some(Rc::new(user_map)), + ); + + let root = Rc::new(root); + root.finish(Rc::clone(&root)); + + // sanity check resource map setup + + assert!(root.has_resource("/info")); + assert!(!root.has_resource("/bar")); + + assert!(root.has_resource("/user/22")); + assert!(root.has_resource("/user/22/")); + assert!(root.has_resource("/user/22/post/55")); + + // extract patterns from paths + + assert!(root.match_name("/bar").is_none()); + assert!(root.match_name("/v44").is_none()); + + assert_eq!(root.match_name("/info"), Some("root_info")); + assert_eq!(root.match_name("/user/22"), None); + assert_eq!(root.match_name("/user/22/"), None); + assert_eq!(root.match_name("/user/22/post/55"), Some("user_post")); + } } diff --git a/src/service.rs b/src/service.rs index f7e201779..cba852a9c 100644 --- a/src/service.rs +++ b/src/service.rs @@ -195,7 +195,13 @@ impl ServiceRequest { pub fn match_info(&self) -> &Path { self.0.match_info() } - + + /// Counterpart to [`HttpRequest::match_name`](../struct.HttpRequest.html#method.match_name). + #[inline] + pub fn match_name(&self) -> Option<&str> { + self.0.match_name() + } + /// Counterpart to [`HttpRequest::match_pattern`](../struct.HttpRequest.html#method.match_pattern). #[inline] pub fn match_pattern(&self) -> Option {