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:
parent
a70e599ff5
commit
fa28175a74
4 changed files with 168 additions and 3 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
119
src/rmap.rs
119
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<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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -195,6 +195,12 @@ impl ServiceRequest {
|
|||
pub fn match_info(&self) -> &Path<Url> {
|
||||
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.
|
||||
|
|
Loading…
Reference in a new issue