1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-30 10:42:55 +01: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 ### Added
* Re-export `actix_rt::main` as `actix_web::main`. * Re-export `actix_rt::main` as `actix_web::main`.
* `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched
resource pattern.
### Changed ### Changed

View File

@ -126,6 +126,17 @@ impl HttpRequest {
&mut Rc::get_mut(&mut self.0).unwrap().path &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 /// Request extensions
#[inline] #[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> { pub fn extensions(&self) -> Ref<'_, Extensions> {
@ -141,7 +152,6 @@ impl HttpRequest {
/// Generate url for named resource /// Generate url for named resource
/// ///
/// ```rust /// ```rust
/// # extern crate actix_web;
/// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// # use actix_web::{web, App, HttpRequest, HttpResponse};
/// # /// #
/// fn index(req: HttpRequest) -> HttpResponse { /// fn index(req: HttpRequest) -> HttpResponse {
@ -599,4 +609,36 @@ mod tests {
assert!(tracker.borrow().dropped); 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 /// Generate url for named resource
/// ///
/// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method.
@ -95,6 +93,45 @@ impl ResourceMap {
false 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>( fn patterns_for<U, I>(
&self, &self,
name: &str, 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

@ -195,6 +195,12 @@ impl ServiceRequest {
pub fn match_info(&self) -> &Path<Url> { pub fn match_info(&self) -> &Path<Url> {
self.0.match_info() 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] #[inline]
/// Get a mutable reference to the Path parameters. /// Get a mutable reference to the Path parameters.