add case insensitive routing to web

This commit is contained in:
Nikolay Kim 2020-03-22 20:29:57 +06:00
parent 00996336fa
commit 48b4c64cbb
7 changed files with 179 additions and 27 deletions

View file

@ -2,6 +2,8 @@
## [0.3.0] - 2020-03-22 ## [0.3.0] - 2020-03-22
* Case insensitive routing
* Use prefix tree for underling data representation * Use prefix tree for underling data representation
## [0.2.4] - 2019-12-31 ## [0.2.4] - 2019-12-31

View file

@ -14,12 +14,14 @@ pub struct ResourceInfo {
pub struct Router<T, U = ()> { pub struct Router<T, U = ()> {
tree: Tree, tree: Tree,
resources: Vec<(ResourceDef, T, Option<U>)>, resources: Vec<(ResourceDef, T, Option<U>)>,
insensitive: bool,
} }
impl<T, U> Router<T, U> { impl<T, U> Router<T, U> {
pub fn build() -> RouterBuilder<T, U> { pub fn build() -> RouterBuilder<T, U> {
RouterBuilder { RouterBuilder {
resources: Vec::new(), resources: Vec::new(),
insensitive: false,
} }
} }
@ -28,7 +30,11 @@ impl<T, U> Router<T, U> {
R: Resource<P>, R: Resource<P>,
P: ResourcePath, 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]; let item = &self.resources[idx];
Some((&item.1, ResourceId(item.0.id()))) Some((&item.1, ResourceId(item.0.id())))
} else { } else {
@ -44,7 +50,11 @@ impl<T, U> Router<T, U> {
R: Resource<P>, R: Resource<P>,
P: ResourcePath, 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]; let item = &mut self.resources[idx];
Some((&mut item.1, ResourceId(item.0.id()))) Some((&mut item.1, ResourceId(item.0.id())))
} else { } else {
@ -62,10 +72,17 @@ impl<T, U> Router<T, U> {
R: Resource<P>, R: Resource<P>,
P: ResourcePath, P: ResourcePath,
{ {
if let Some(idx) = self.tree.find_checked(resource, &|idx, res| { if let Some(idx) = if self.insensitive {
let item = &self.resources[idx]; self.tree.find_checked_insensitive(resource, &|idx, res| {
check(res, item.2.as_ref()) 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]; let item = &mut self.resources[idx];
Some((&mut item.1, ResourceId(item.0.id()))) Some((&mut item.1, ResourceId(item.0.id())))
} else { } else {
@ -75,10 +92,19 @@ impl<T, U> Router<T, U> {
} }
pub struct RouterBuilder<T, U = ()> { pub struct RouterBuilder<T, U = ()> {
insensitive: bool,
resources: Vec<(ResourceDef, T, Option<U>)>, resources: Vec<(ResourceDef, T, Option<U>)>,
} }
impl<T, U> RouterBuilder<T, 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. /// Register resource for specified path.
pub fn path<P: IntoPattern>( pub fn path<P: IntoPattern>(
&mut self, &mut self,
@ -126,6 +152,7 @@ impl<T, U> RouterBuilder<T, U> {
Router { Router {
tree, tree,
resources: self.resources, resources: self.resources,
insensitive: self.insensitive,
} }
} }
} }
@ -235,6 +262,26 @@ mod tests {
assert_eq!(*h, 11); 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] #[test]
fn test_recognizer_with_path_skip() { fn test_recognizer_with_path_skip() {
let mut router = Router::<usize>::build(); let mut router = Router::<usize>::build();

View file

@ -150,7 +150,15 @@ impl Tree {
T: ResourcePath, T: ResourcePath,
R: Resource<T>, 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>( pub(crate) fn find_checked<T, R, F>(
@ -158,6 +166,33 @@ impl Tree {
resource: &mut R, resource: &mut R,
check: &F, check: &F,
) -> Option<usize> ) -> 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 where
T: ResourcePath, T: ResourcePath,
R: Resource<T>, R: Resource<T>,
@ -192,7 +227,9 @@ impl Tree {
let res = self let res = self
.children .children
.iter() .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) .filter_map(|x| x)
.next(); .next();
@ -211,7 +248,7 @@ impl Tree {
} }
if let Some((val, skip)) = 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(); let path = resource.resource_path();
path.segments = segments; path.segments = segments;
@ -229,6 +266,7 @@ impl Tree {
check: &F, check: &F,
mut skip: usize, mut skip: usize,
segments: &mut Vec<(&'static str, PathItem)>, segments: &mut Vec<(&'static str, PathItem)>,
insensitive: bool,
) -> Option<(usize, usize)> ) -> Option<(usize, usize)>
where where
T: ResourcePath, T: ResourcePath,
@ -248,7 +286,13 @@ impl Tree {
// check segment match // check segment match
let is_match = match key[0] { 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 { Segment::Dynamic {
ref pattern, ref pattern,
ref names, ref names,
@ -365,7 +409,16 @@ impl Tree {
return self return self
.children .children
.iter() .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) .filter_map(|x| x)
.next(); .next();
} else { } else {

View file

@ -42,6 +42,7 @@ pub struct App<T, B, Err = DefaultError> {
external: Vec<ResourceDef>, external: Vec<ResourceDef>,
extensions: Extensions, extensions: Extensions,
error_renderer: Err, error_renderer: Err,
case_insensitive: bool,
_t: PhantomData<B>, _t: PhantomData<B>,
} }
@ -59,6 +60,7 @@ impl App<AppEntry<DefaultError>, Body, DefaultError> {
external: Vec::new(), external: Vec::new(),
extensions: Extensions::new(), extensions: Extensions::new(),
error_renderer: DefaultError, error_renderer: DefaultError,
case_insensitive: false,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -78,6 +80,7 @@ impl<Err> App<AppEntry<Err>, Body, Err> {
external: Vec::new(), external: Vec::new(),
extensions: Extensions::new(), extensions: Extensions::new(),
error_renderer: err, error_renderer: err,
case_insensitive: false,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -404,6 +407,7 @@ where
external: self.external, external: self.external,
extensions: self.extensions, extensions: self.extensions,
error_renderer: self.error_renderer, error_renderer: self.error_renderer,
case_insensitive: self.case_insensitive,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -468,9 +472,18 @@ where
external: self.external, external: self.external,
extensions: self.extensions, extensions: self.extensions,
error_renderer: self.error_renderer, error_renderer: self.error_renderer,
case_insensitive: self.case_insensitive,
_t: PhantomData, _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> impl<T, B, Err> IntoServiceFactory<AppInit<T, B, Err>> for App<T, B, Err>
@ -495,6 +508,7 @@ where
default: self.default, default: self.default,
factory_ref: self.factory_ref, factory_ref: self.factory_ref,
extensions: RefCell::new(Some(self.extensions)), 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] #[actix_rt::test]
async fn test_external_resource() { async fn test_external_resource() {
let mut srv = init_service( let mut srv = init_service(

View file

@ -48,6 +48,7 @@ where
pub(super) default: Option<Rc<HttpNewService<Err>>>, pub(super) default: Option<Rc<HttpNewService<Err>>>,
pub(super) factory_ref: Rc<RefCell<Option<AppRoutingFactory<Err>>>>, pub(super) factory_ref: Rc<RefCell<Option<AppRoutingFactory<Err>>>>,
pub(super) external: RefCell<Vec<ResourceDef>>, pub(super) external: RefCell<Vec<ResourceDef>>,
pub(super) case_insensitive: bool,
} }
impl<T, B, Err> ServiceFactory for AppInit<T, B, Err> impl<T, B, Err> ServiceFactory for AppInit<T, B, Err>
@ -101,6 +102,7 @@ where
}) })
.collect(), .collect(),
), ),
case_insensitive: self.case_insensitive,
}); });
// external resources // external resources
@ -118,6 +120,7 @@ where
data: self.data.clone(), data: self.data.clone(),
data_factories: Vec::new(), data_factories: Vec::new(),
data_factories_fut: self.data_factories.iter().map(|f| f()).collect(), data_factories_fut: self.data_factories.iter().map(|f| f()).collect(),
case_insensitive: self.case_insensitive,
extensions: Some( extensions: Some(
self.extensions self.extensions
.borrow_mut() .borrow_mut()
@ -144,6 +147,7 @@ where
data: Rc<Vec<Box<dyn DataFactory>>>, data: Rc<Vec<Box<dyn DataFactory>>>,
data_factories: Vec<Box<dyn DataFactory>>, data_factories: Vec<Box<dyn DataFactory>>,
data_factories_fut: Vec<LocalBoxFuture<'static, Result<Box<dyn DataFactory>, ()>>>, data_factories_fut: Vec<LocalBoxFuture<'static, Result<Box<dyn DataFactory>, ()>>>,
case_insensitive: bool,
extensions: Option<Extensions>, extensions: Option<Extensions>,
_t: PhantomData<(B, Err)>, _t: PhantomData<(B, Err)>,
} }
@ -267,6 +271,7 @@ where
pub struct AppRoutingFactory<Err> { pub struct AppRoutingFactory<Err> {
services: Rc<Vec<(ResourceDef, HttpNewService<Err>, RefCell<Option<Guards>>)>>, services: Rc<Vec<(ResourceDef, HttpNewService<Err>, RefCell<Option<Guards>>)>>,
default: Rc<HttpNewService<Err>>, default: Rc<HttpNewService<Err>>,
case_insensitive: bool,
} }
impl<Err: 'static> ServiceFactory for AppRoutingFactory<Err> { impl<Err: 'static> ServiceFactory for AppRoutingFactory<Err> {
@ -293,6 +298,7 @@ impl<Err: 'static> ServiceFactory for AppRoutingFactory<Err> {
.collect(), .collect(),
default: None, default: None,
default_fut: Some(self.default.new_service(())), default_fut: Some(self.default.new_service(())),
case_insensitive: self.case_insensitive,
} }
} }
} }
@ -305,6 +311,7 @@ pub struct AppRoutingFactoryResponse<Err> {
fut: Vec<CreateAppRoutingItem<Err>>, fut: Vec<CreateAppRoutingItem<Err>>,
default: Option<HttpService<Err>>, default: Option<HttpService<Err>>,
default_fut: Option<LocalBoxFuture<'static, Result<HttpService<Err>, ()>>>, default_fut: Option<LocalBoxFuture<'static, Result<HttpService<Err>, ()>>>,
case_insensitive: bool,
} }
enum CreateAppRoutingItem<Err> { enum CreateAppRoutingItem<Err> {
@ -351,18 +358,23 @@ impl<Err> Future for AppRoutingFactoryResponse<Err> {
} }
if done { if done {
let router = self let mut router =
.fut self.fut
.drain(..) .drain(..)
.fold(Router::build(), |mut router, item| { .fold(Router::build(), |mut router, item| {
match item { match item {
CreateAppRoutingItem::Service(path, guards, service) => { CreateAppRoutingItem::Service(path, guards, service) => {
router.rdef(path, service).2 = guards; router.rdef(path, service).2 = guards;
}
CreateAppRoutingItem::Future(_, _, _) => unreachable!(),
} }
CreateAppRoutingItem::Future(_, _, _) => unreachable!(), router
} });
router
}); if self.case_insensitive {
router.case_insensitive();
}
Poll::Ready(Ok(AppRouting { Poll::Ready(Ok(AppRouting {
ready: None, ready: None,
router: router.finish(), router: router.finish(),

View file

@ -532,7 +532,6 @@ mod tests {
Ok(()) Ok(())
}; };
let s = format!("{}", FormatDisplay(&render)); let s = format!("{}", FormatDisplay(&render));
println!("{}", s);
assert!(s.contains("/test/route/yeah")); assert!(s.contains("/test/route/yeah"));
} }

View file

@ -431,6 +431,7 @@ where
.into_iter() .into_iter()
.for_each(|mut srv| srv.register(&mut cfg)); .for_each(|mut srv| srv.register(&mut cfg));
let slesh = self.rdef.ends_with('/');
let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef)); let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef));
// external resources // external resources
@ -451,7 +452,14 @@ where
cfg.into_services() cfg.into_services()
.1 .1
.into_iter() .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); rmap.add(&mut rdef, nested);
(rdef, srv, RefCell::new(guards)) (rdef, srv, RefCell::new(guards))
}) })
@ -725,9 +733,9 @@ mod tests {
) )
.await; .await;
let req = TestRequest::with_uri("/app").to_request(); // let req = TestRequest::with_uri("/app").to_request();
let resp = srv.call(req).await.unwrap(); // let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND); // assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let req = TestRequest::with_uri("/app/").to_request(); let req = TestRequest::with_uri("/app/").to_request();
let resp = srv.call(req).await.unwrap(); let resp = srv.call(req).await.unwrap();