mirror of
https://github.com/ntex-rs/ntex.git
synced 2025-04-04 13:27:39 +03:00
add case insensitive routing to web
This commit is contained in:
parent
00996336fa
commit
48b4c64cbb
7 changed files with 179 additions and 27 deletions
|
@ -2,6 +2,8 @@
|
|||
|
||||
## [0.3.0] - 2020-03-22
|
||||
|
||||
* Case insensitive routing
|
||||
|
||||
* Use prefix tree for underling data representation
|
||||
|
||||
## [0.2.4] - 2019-12-31
|
||||
|
|
|
@ -14,12 +14,14 @@ pub struct ResourceInfo {
|
|||
pub struct Router<T, U = ()> {
|
||||
tree: Tree,
|
||||
resources: Vec<(ResourceDef, T, Option<U>)>,
|
||||
insensitive: bool,
|
||||
}
|
||||
|
||||
impl<T, U> Router<T, U> {
|
||||
pub fn build() -> RouterBuilder<T, U> {
|
||||
RouterBuilder {
|
||||
resources: Vec::new(),
|
||||
insensitive: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,7 +30,11 @@ impl<T, U> Router<T, U> {
|
|||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
{
|
||||
if let Some(idx) = self.tree.find(resource) {
|
||||
if let Some(idx) = if self.insensitive {
|
||||
self.tree.find_insensitive(resource)
|
||||
} else {
|
||||
self.tree.find(resource)
|
||||
} {
|
||||
let item = &self.resources[idx];
|
||||
Some((&item.1, ResourceId(item.0.id())))
|
||||
} else {
|
||||
|
@ -44,7 +50,11 @@ impl<T, U> Router<T, U> {
|
|||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
{
|
||||
if let Some(idx) = self.tree.find(resource) {
|
||||
if let Some(idx) = if self.insensitive {
|
||||
self.tree.find_insensitive(resource)
|
||||
} else {
|
||||
self.tree.find(resource)
|
||||
} {
|
||||
let item = &mut self.resources[idx];
|
||||
Some((&mut item.1, ResourceId(item.0.id())))
|
||||
} else {
|
||||
|
@ -62,10 +72,17 @@ impl<T, U> Router<T, U> {
|
|||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
{
|
||||
if let Some(idx) = self.tree.find_checked(resource, &|idx, res| {
|
||||
if let Some(idx) = if self.insensitive {
|
||||
self.tree.find_checked_insensitive(resource, &|idx, res| {
|
||||
let item = &self.resources[idx];
|
||||
check(res, item.2.as_ref())
|
||||
}) {
|
||||
})
|
||||
} else {
|
||||
self.tree.find_checked(resource, &|idx, res| {
|
||||
let item = &self.resources[idx];
|
||||
check(res, item.2.as_ref())
|
||||
})
|
||||
} {
|
||||
let item = &mut self.resources[idx];
|
||||
Some((&mut item.1, ResourceId(item.0.id())))
|
||||
} else {
|
||||
|
@ -75,10 +92,19 @@ impl<T, U> Router<T, U> {
|
|||
}
|
||||
|
||||
pub struct RouterBuilder<T, U = ()> {
|
||||
insensitive: bool,
|
||||
resources: Vec<(ResourceDef, T, Option<U>)>,
|
||||
}
|
||||
|
||||
impl<T, U> RouterBuilder<T, U> {
|
||||
/// Make router case insensitive. Only static segments
|
||||
/// could be case insensitive.
|
||||
///
|
||||
/// By default router is case sensitive.
|
||||
pub fn case_insensitive(&mut self) {
|
||||
self.insensitive = true;
|
||||
}
|
||||
|
||||
/// Register resource for specified path.
|
||||
pub fn path<P: IntoPattern>(
|
||||
&mut self,
|
||||
|
@ -126,6 +152,7 @@ impl<T, U> RouterBuilder<T, U> {
|
|||
Router {
|
||||
tree,
|
||||
resources: self.resources,
|
||||
insensitive: self.insensitive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -235,6 +262,26 @@ mod tests {
|
|||
assert_eq!(*h, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recognizer_3() {
|
||||
let mut router = Router::<usize>::build();
|
||||
router.path("/index.json", 10);
|
||||
router.path("/{source}.json", 11);
|
||||
router.case_insensitive();
|
||||
let mut router = router.finish();
|
||||
|
||||
let mut path = Path::new("/index.json");
|
||||
let (h, _) = router.recognize_mut(&mut path).unwrap();
|
||||
assert_eq!(*h, 10);
|
||||
|
||||
let mut path = Path::new("/indeX.json");
|
||||
let (h, _) = router.recognize_mut(&mut path).unwrap();
|
||||
assert_eq!(*h, 10);
|
||||
|
||||
let mut path = Path::new("/test.jsoN");
|
||||
assert!(router.recognize_mut(&mut path).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recognizer_with_path_skip() {
|
||||
let mut router = Router::<usize>::build();
|
||||
|
|
|
@ -150,7 +150,15 @@ impl Tree {
|
|||
T: ResourcePath,
|
||||
R: Resource<T>,
|
||||
{
|
||||
self.find_checked(resource, &|_, _| true)
|
||||
self.find_checked_inner(resource, false, &|_, _| true)
|
||||
}
|
||||
|
||||
pub(crate) fn find_insensitive<T, R>(&self, resource: &mut R) -> Option<usize>
|
||||
where
|
||||
T: ResourcePath,
|
||||
R: Resource<T>,
|
||||
{
|
||||
self.find_checked_inner(resource, true, &|_, _| true)
|
||||
}
|
||||
|
||||
pub(crate) fn find_checked<T, R, F>(
|
||||
|
@ -158,6 +166,33 @@ impl Tree {
|
|||
resource: &mut R,
|
||||
check: &F,
|
||||
) -> Option<usize>
|
||||
where
|
||||
T: ResourcePath,
|
||||
R: Resource<T>,
|
||||
F: Fn(usize, &R) -> bool,
|
||||
{
|
||||
self.find_checked_inner(resource, false, check)
|
||||
}
|
||||
|
||||
pub(crate) fn find_checked_insensitive<T, R, F>(
|
||||
&self,
|
||||
resource: &mut R,
|
||||
check: &F,
|
||||
) -> Option<usize>
|
||||
where
|
||||
T: ResourcePath,
|
||||
R: Resource<T>,
|
||||
F: Fn(usize, &R) -> bool,
|
||||
{
|
||||
self.find_checked_inner(resource, true, check)
|
||||
}
|
||||
|
||||
pub(crate) fn find_checked_inner<T, R, F>(
|
||||
&self,
|
||||
resource: &mut R,
|
||||
insensitive: bool,
|
||||
check: &F,
|
||||
) -> Option<usize>
|
||||
where
|
||||
T: ResourcePath,
|
||||
R: Resource<T>,
|
||||
|
@ -192,7 +227,9 @@ impl Tree {
|
|||
let res = self
|
||||
.children
|
||||
.iter()
|
||||
.map(|x| x.find_inner2(path, resource, check, 1, &mut segments))
|
||||
.map(|x| {
|
||||
x.find_inner2(path, resource, check, 1, &mut segments, insensitive)
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.next();
|
||||
|
||||
|
@ -211,7 +248,7 @@ impl Tree {
|
|||
}
|
||||
|
||||
if let Some((val, skip)) =
|
||||
self.find_inner2(path, resource, check, 1, &mut segments)
|
||||
self.find_inner2(path, resource, check, 1, &mut segments, insensitive)
|
||||
{
|
||||
let path = resource.resource_path();
|
||||
path.segments = segments;
|
||||
|
@ -229,6 +266,7 @@ impl Tree {
|
|||
check: &F,
|
||||
mut skip: usize,
|
||||
segments: &mut Vec<(&'static str, PathItem)>,
|
||||
insensitive: bool,
|
||||
) -> Option<(usize, usize)>
|
||||
where
|
||||
T: ResourcePath,
|
||||
|
@ -248,7 +286,13 @@ impl Tree {
|
|||
|
||||
// check segment match
|
||||
let is_match = match key[0] {
|
||||
Segment::Static(ref pattern) => pattern == segment.as_ref(),
|
||||
Segment::Static(ref pattern) => {
|
||||
if insensitive {
|
||||
pattern.eq_ignore_ascii_case(segment.as_ref())
|
||||
} else {
|
||||
pattern == segment.as_ref()
|
||||
}
|
||||
}
|
||||
Segment::Dynamic {
|
||||
ref pattern,
|
||||
ref names,
|
||||
|
@ -365,7 +409,16 @@ impl Tree {
|
|||
return self
|
||||
.children
|
||||
.iter()
|
||||
.map(|x| x.find_inner2(path, resource, check, skip, segments))
|
||||
.map(|x| {
|
||||
x.find_inner2(
|
||||
path,
|
||||
resource,
|
||||
check,
|
||||
skip,
|
||||
segments,
|
||||
insensitive,
|
||||
)
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.next();
|
||||
} else {
|
||||
|
|
|
@ -42,6 +42,7 @@ pub struct App<T, B, Err = DefaultError> {
|
|||
external: Vec<ResourceDef>,
|
||||
extensions: Extensions,
|
||||
error_renderer: Err,
|
||||
case_insensitive: bool,
|
||||
_t: PhantomData<B>,
|
||||
}
|
||||
|
||||
|
@ -59,6 +60,7 @@ impl App<AppEntry<DefaultError>, Body, DefaultError> {
|
|||
external: Vec::new(),
|
||||
extensions: Extensions::new(),
|
||||
error_renderer: DefaultError,
|
||||
case_insensitive: false,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
@ -78,6 +80,7 @@ impl<Err> App<AppEntry<Err>, Body, Err> {
|
|||
external: Vec::new(),
|
||||
extensions: Extensions::new(),
|
||||
error_renderer: err,
|
||||
case_insensitive: false,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
@ -404,6 +407,7 @@ where
|
|||
external: self.external,
|
||||
extensions: self.extensions,
|
||||
error_renderer: self.error_renderer,
|
||||
case_insensitive: self.case_insensitive,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
@ -468,9 +472,18 @@ where
|
|||
external: self.external,
|
||||
extensions: self.extensions,
|
||||
error_renderer: self.error_renderer,
|
||||
case_insensitive: self.case_insensitive,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use ascii case-insensitive routing.
|
||||
///
|
||||
/// Only static segments could be case-insensitive.
|
||||
pub fn case_insensitive_routing(mut self) -> Self {
|
||||
self.case_insensitive = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, B, Err> IntoServiceFactory<AppInit<T, B, Err>> for App<T, B, Err>
|
||||
|
@ -495,6 +508,7 @@ where
|
|||
default: self.default,
|
||||
factory_ref: self.factory_ref,
|
||||
extensions: RefCell::new(Some(self.extensions)),
|
||||
case_insensitive: self.case_insensitive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -692,6 +706,23 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_case_insensitive_router() {
|
||||
let mut srv = init_service(
|
||||
App::new()
|
||||
.case_insensitive_routing()
|
||||
.route("/test", web::get().to(|| async { HttpResponse::Ok() })),
|
||||
)
|
||||
.await;
|
||||
let req = TestRequest::with_uri("/test").to_request();
|
||||
let resp = call_service(&mut srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/Test").to_request();
|
||||
let resp = call_service(&mut srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_external_resource() {
|
||||
let mut srv = init_service(
|
||||
|
|
|
@ -48,6 +48,7 @@ where
|
|||
pub(super) default: Option<Rc<HttpNewService<Err>>>,
|
||||
pub(super) factory_ref: Rc<RefCell<Option<AppRoutingFactory<Err>>>>,
|
||||
pub(super) external: RefCell<Vec<ResourceDef>>,
|
||||
pub(super) case_insensitive: bool,
|
||||
}
|
||||
|
||||
impl<T, B, Err> ServiceFactory for AppInit<T, B, Err>
|
||||
|
@ -101,6 +102,7 @@ where
|
|||
})
|
||||
.collect(),
|
||||
),
|
||||
case_insensitive: self.case_insensitive,
|
||||
});
|
||||
|
||||
// external resources
|
||||
|
@ -118,6 +120,7 @@ where
|
|||
data: self.data.clone(),
|
||||
data_factories: Vec::new(),
|
||||
data_factories_fut: self.data_factories.iter().map(|f| f()).collect(),
|
||||
case_insensitive: self.case_insensitive,
|
||||
extensions: Some(
|
||||
self.extensions
|
||||
.borrow_mut()
|
||||
|
@ -144,6 +147,7 @@ where
|
|||
data: Rc<Vec<Box<dyn DataFactory>>>,
|
||||
data_factories: Vec<Box<dyn DataFactory>>,
|
||||
data_factories_fut: Vec<LocalBoxFuture<'static, Result<Box<dyn DataFactory>, ()>>>,
|
||||
case_insensitive: bool,
|
||||
extensions: Option<Extensions>,
|
||||
_t: PhantomData<(B, Err)>,
|
||||
}
|
||||
|
@ -267,6 +271,7 @@ where
|
|||
pub struct AppRoutingFactory<Err> {
|
||||
services: Rc<Vec<(ResourceDef, HttpNewService<Err>, RefCell<Option<Guards>>)>>,
|
||||
default: Rc<HttpNewService<Err>>,
|
||||
case_insensitive: bool,
|
||||
}
|
||||
|
||||
impl<Err: 'static> ServiceFactory for AppRoutingFactory<Err> {
|
||||
|
@ -293,6 +298,7 @@ impl<Err: 'static> ServiceFactory for AppRoutingFactory<Err> {
|
|||
.collect(),
|
||||
default: None,
|
||||
default_fut: Some(self.default.new_service(())),
|
||||
case_insensitive: self.case_insensitive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -305,6 +311,7 @@ pub struct AppRoutingFactoryResponse<Err> {
|
|||
fut: Vec<CreateAppRoutingItem<Err>>,
|
||||
default: Option<HttpService<Err>>,
|
||||
default_fut: Option<LocalBoxFuture<'static, Result<HttpService<Err>, ()>>>,
|
||||
case_insensitive: bool,
|
||||
}
|
||||
|
||||
enum CreateAppRoutingItem<Err> {
|
||||
|
@ -351,8 +358,8 @@ impl<Err> Future for AppRoutingFactoryResponse<Err> {
|
|||
}
|
||||
|
||||
if done {
|
||||
let router = self
|
||||
.fut
|
||||
let mut router =
|
||||
self.fut
|
||||
.drain(..)
|
||||
.fold(Router::build(), |mut router, item| {
|
||||
match item {
|
||||
|
@ -363,6 +370,11 @@ impl<Err> Future for AppRoutingFactoryResponse<Err> {
|
|||
}
|
||||
router
|
||||
});
|
||||
|
||||
if self.case_insensitive {
|
||||
router.case_insensitive();
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(AppRouting {
|
||||
ready: None,
|
||||
router: router.finish(),
|
||||
|
|
|
@ -532,7 +532,6 @@ mod tests {
|
|||
Ok(())
|
||||
};
|
||||
let s = format!("{}", FormatDisplay(&render));
|
||||
println!("{}", s);
|
||||
assert!(s.contains("/test/route/yeah"));
|
||||
}
|
||||
|
||||
|
|
|
@ -431,6 +431,7 @@ where
|
|||
.into_iter()
|
||||
.for_each(|mut srv| srv.register(&mut cfg));
|
||||
|
||||
let slesh = self.rdef.ends_with('/');
|
||||
let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef));
|
||||
|
||||
// external resources
|
||||
|
@ -451,7 +452,14 @@ where
|
|||
cfg.into_services()
|
||||
.1
|
||||
.into_iter()
|
||||
.map(|(mut rdef, srv, guards, nested)| {
|
||||
.map(|(rdef, srv, guards, nested)| {
|
||||
// case for scope prefix ends with '/' and
|
||||
// resource is empty pattern
|
||||
let mut rdef = if slesh && rdef.pattern() == "" {
|
||||
ResourceDef::new("/")
|
||||
} else {
|
||||
rdef
|
||||
};
|
||||
rmap.add(&mut rdef, nested);
|
||||
(rdef, srv, RefCell::new(guards))
|
||||
})
|
||||
|
@ -725,9 +733,9 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::with_uri("/app").to_request();
|
||||
let resp = srv.call(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
// let req = TestRequest::with_uri("/app").to_request();
|
||||
// let resp = srv.call(req).await.unwrap();
|
||||
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/app/").to_request();
|
||||
let resp = srv.call(req).await.unwrap();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue