diff --git a/CHANGES.md b/CHANGES.md index 58cddf78..22a389d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,8 @@ ### Added * Re-export `actix_rt::main` as `actix_web::main`. +* `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched + resource pattern. ### Changed diff --git a/src/request.rs b/src/request.rs index f8abeb1b..8ca89744 100644 --- a/src/request.rs +++ b/src/request.rs @@ -126,6 +126,17 @@ impl HttpRequest { &mut Rc::get_mut(&mut self.0).unwrap().path } + /// The resource definition pattern that matched the path. Useful for logging and metrics. + /// + /// For example, when a resource with pattern `/user/{id}/profile` is defined and a call is made + /// to `/user/123/profile` this function would return `Some("/user/{id}/profile")`. + /// + /// Returns a None when no resource is fully matched, including default services. + #[inline] + pub fn match_pattern(&self) -> Option { + self.0.rmap.match_pattern(self.path()) + } + /// Request extensions #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { @@ -141,7 +152,6 @@ impl HttpRequest { /// Generate url for named resource /// /// ```rust - /// # extern crate actix_web; /// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// # /// fn index(req: HttpRequest) -> HttpResponse { @@ -599,4 +609,36 @@ mod tests { assert!(tracker.borrow().dropped); } + + #[actix_rt::test] + async fn extract_path_pattern() { + let mut srv = init_service( + App::new().service( + web::scope("/user/{id}") + .service(web::resource("/profile").route(web::get().to( + move |req: HttpRequest| { + assert_eq!( + req.match_pattern(), + Some("/user/{id}/profile".to_owned()) + ); + + HttpResponse::Ok().finish() + }, + ))) + .default_service(web::to(move |req: HttpRequest| { + assert!(req.match_pattern().is_none()); + HttpResponse::Ok().finish() + })), + ), + ) + .await; + + let req = TestRequest::get().uri("/user/22/profile").to_request(); + let res = call_service(&mut srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + + let req = TestRequest::get().uri("/user/22/not-exist").to_request(); + let res = call_service(&mut srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + } } diff --git a/src/rmap.rs b/src/rmap.rs index 47092608..0a0c9677 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -43,9 +43,7 @@ impl ResourceMap { } } } -} -impl ResourceMap { /// Generate url for named resource /// /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. @@ -95,6 +93,45 @@ impl ResourceMap { false } + /// 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 { + let path = if path.is_empty() { "/" } else { path }; + + // ensure a full match exists + if !self.has_resource(path) { + return None; + } + + Some(self.traverse_resource_pattern(path)) + } + + /// Takes remaining path and tries to match it up against a resource definition within the + /// current resource map recursively, returning a concatenation of all resource prefixes and + /// patterns matched in the tree. + /// + /// Should only be used after checking the resource exists in the map so that partial match + /// patterns are not returned. + fn traverse_resource_pattern(&self, remaining: &str) -> String { + for (pattern, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if let Some(prefix_len) = pattern.is_prefix_match(remaining) { + let prefix = pattern.pattern().to_owned(); + + return [ + prefix, + rmap.traverse_resource_pattern(&remaining[prefix_len..]), + ] + .concat(); + } + } else if pattern.is_match(remaining) { + return pattern.pattern().to_owned(); + } + } + + String::new() + } + fn patterns_for( &self, name: &str, @@ -188,3 +225,81 @@ impl ResourceMap { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_matched_pattern() { + let mut root = ResourceMap::new(ResourceDef::root_prefix("")); + + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + user_map.add(&mut ResourceDef::new("/"), None); + user_map.add(&mut ResourceDef::new("/profile"), None); + user_map.add(&mut ResourceDef::new("/article/{id}"), None); + user_map.add(&mut ResourceDef::new("/post/{post_id}"), None); + user_map.add( + &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"), + None, + ); + + root.add(&mut ResourceDef::new("/info"), None); + root.add(&mut ResourceDef::new("/v{version:[[:digit:]]{1}}"), 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("/v1")); + assert!(root.has_resource("/v2")); + assert!(!root.has_resource("/v33")); + + assert!(root.has_resource("/user/22")); + assert!(root.has_resource("/user/22/")); + assert!(root.has_resource("/user/22/profile")); + + // extract patterns from paths + + assert!(root.match_pattern("/bar").is_none()); + assert!(root.match_pattern("/v44").is_none()); + + assert_eq!(root.match_pattern("/info"), Some("/info".to_owned())); + assert_eq!( + root.match_pattern("/v1"), + Some("/v{version:[[:digit:]]{1}}".to_owned()) + ); + assert_eq!( + root.match_pattern("/v2"), + Some("/v{version:[[:digit:]]{1}}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/profile"), + Some("/user/{id}/profile".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/602CFB82-7709-4B17-ADCF-4C347B6F2203/profile"), + Some("/user/{id}/profile".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/article/44"), + Some("/user/{id}/article/{id}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/post/my-post"), + Some("/user/{id}/post/{post_id}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/post/other-post/comment/42"), + Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned()) + ); + } +} diff --git a/src/service.rs b/src/service.rs index 232a2f13..f7e20177 100644 --- a/src/service.rs +++ b/src/service.rs @@ -195,6 +195,12 @@ impl ServiceRequest { pub fn match_info(&self) -> &Path { self.0.match_info() } + + /// Counterpart to [`HttpRequest::match_pattern`](../struct.HttpRequest.html#method.match_pattern). + #[inline] + pub fn match_pattern(&self) -> Option { + self.0.match_pattern() + } #[inline] /// Get a mutable reference to the Path parameters.