1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-11-20 08:31:09 +00:00

add method to extract matched resource pattern (#1566)

This commit is contained in:
Rob Ede 2020-06-23 00:58:20 +01:00 committed by GitHub
parent a70e599ff5
commit fa28175a74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 168 additions and 3 deletions

View file

@ -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

View file

@ -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<String> {
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);
}
}

View file

@ -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<String> {
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<U, I>(
&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())
);
}
}

View file

@ -196,6 +196,12 @@ impl ServiceRequest {
self.0.match_info()
}
/// Counterpart to [`HttpRequest::match_pattern`](../struct.HttpRequest.html#method.match_pattern).
#[inline]
pub fn match_pattern(&self) -> Option<String> {
self.0.match_pattern()
}
#[inline]
/// Get a mutable reference to the Path parameters.
pub fn match_info_mut(&mut self) -> &mut Path<Url> {