[go: up one dir, main page]

Skip to content

Commit

Permalink
"matchit" based router (#363)
Browse files Browse the repository at this point in the history
* "matchit" based router

* Update changelog

* Remove dependency on `regex`

* Docs

* Fix typos

* Also mention route order in root module docs

* Update CHANGELOG.md

Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>

* Document that `/:key` and `/foo` overlaps

* Provide good error message for wildcards in routes

* minor clean ups

* Make `Router` cheaper to clone

* Ensure middleware still only applies to routes above

* Remove call to issues from changelog

We're aware of the short coming :)

* Fix tests on 1.51

Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
  • Loading branch information
davidpdrsn and jplatte authored Oct 24, 2021
1 parent 9fcc884 commit 1a78a3f
Show file tree
Hide file tree
Showing 11 changed files with 539 additions and 399 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

# Unreleased

- Big internal refactoring of routing leading to several improvements ([#363])
- Wildcard routes like `.route("/api/users/*rest", service)` are now supported.
- The order routes are added in no longer matters.
- Adding a conflicting route will now cause a panic instead of silently making
a route unreachable.
- Route matching is faster as number of routes increase.
- The routes `/foo` and `/:key` are considered to overlap and will cause a
panic when constructing the router. This might be fixed in the future.
- Improve performance of `BoxRoute` ([#339])
- Expand accepted content types for JSON requests ([#378])
- **breaking:** Automatically do percent decoding in `extract::Path`
Expand All @@ -25,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#286]: https://github.com/tokio-rs/axum/pull/286
[#272]: https://github.com/tokio-rs/axum/pull/272
[#378]: https://github.com/tokio-rs/axum/pull/378
[#363]: https://github.com/tokio-rs/axum/pull/363
[#396]: https://github.com/tokio-rs/axum/pull/396

# 0.2.8 (07. October, 2021)
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ futures-util = { version = "0.3", default-features = false, features = ["alloc"]
http = "0.2"
http-body = "0.4.3"
hyper = { version = "0.14", features = ["server", "tcp", "stream"] }
matchit = "0.4"
percent-encoding = "2.1"
pin-project-lite = "0.2.7"
regex = "1.5"
serde = "1.0"
serde_urlencoded = "0.7"
sync_wrapper = "0.1.1"
Expand All @@ -60,8 +60,8 @@ reqwest = { version = "0.11", features = ["json", "stream"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.6.1", features = ["macros", "rt", "rt-multi-thread", "net"] }
uuid = { version = "0.8", features = ["serde", "v4"] }
tokio-stream = "0.1"
uuid = { version = "0.8", features = ["serde", "v4"] }

[dev-dependencies.tower]
package = "tower"
Expand Down
24 changes: 24 additions & 0 deletions src/extract/path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ mod tests {
use super::*;
use crate::tests::*;
use crate::{handler::get, Router};
use std::collections::HashMap;

#[tokio::test]
async fn percent_decoding() {
Expand All @@ -190,4 +191,27 @@ mod tests {

assert_eq!(res.text().await, "one two");
}

#[tokio::test]
async fn wildcard() {
let app = Router::new()
.route(
"/foo/*rest",
get(|Path(param): Path<String>| async move { param }),
)
.route(
"/bar/*rest",
get(|Path(params): Path<HashMap<String, String>>| async move {
params.get("rest").unwrap().clone()
}),
);

let client = TestClient::new(app);

let res = client.get("/foo/bar/baz").send().await;
assert_eq!(res.text().await, "/bar/baz");

let res = client.get("/bar/baz/qux").send().await;
assert_eq!(res.text().await, "/baz/qux");
}
}
127 changes: 57 additions & 70 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
//! - [Handlers](#handlers)
//! - [Debugging handler type errors](#debugging-handler-type-errors)
//! - [Routing](#routing)
//! - [Precedence](#precedence)
//! - [Matching multiple methods](#matching-multiple-methods)
//! - [Routing to any `Service`](#routing-to-any-service)
//! - [Routing to fallible services](#routing-to-fallible-services)
//! - [Wildcard routes](#wildcard-routes)
//! - [Nesting routes](#nesting-routes)
//! - [Extractors](#extractors)
//! - [Common extractors](#common-extractors)
Expand Down Expand Up @@ -177,75 +177,8 @@
//!
//! You can also define routes separately and merge them with [`Router::or`].
//!
//! ## Precedence
//!
//! Note that routes are matched _bottom to top_ so routes that should have
//! higher precedence should be added _after_ routes with lower precedence:
//!
//! ```rust
//! use axum::{
//! body::{Body, BoxBody},
//! handler::get,
//! http::Request,
//! Router,
//! };
//! use tower::{Service, ServiceExt};
//! use http::{Method, Response, StatusCode};
//! use std::convert::Infallible;
//!
//! # #[tokio::main]
//! # async fn main() {
//! // `/foo` also matches `/:key` so adding the routes in this order means `/foo`
//! // will be inaccessible.
//! let mut app = Router::new()
//! .route("/foo", get(|| async { "/foo called" }))
//! .route("/:key", get(|| async { "/:key called" }));
//!
//! // Even though we use `/foo` as the request URI, `/:key` takes precedence
//! // since its defined last.
//! let (status, body) = call_service(&mut app, Method::GET, "/foo").await;
//! assert_eq!(status, StatusCode::OK);
//! assert_eq!(body, "/:key called");
//!
//! // We have to add `/foo` after `/:key` since routes are matched bottom to
//! // top.
//! let mut new_app = Router::new()
//! .route("/:key", get(|| async { "/:key called" }))
//! .route("/foo", get(|| async { "/foo called" }));
//!
//! // Now it works
//! let (status, body) = call_service(&mut new_app, Method::GET, "/foo").await;
//! assert_eq!(status, StatusCode::OK);
//! assert_eq!(body, "/foo called");
//!
//! // And the other route works as well
//! let (status, body) = call_service(&mut new_app, Method::GET, "/bar").await;
//! assert_eq!(status, StatusCode::OK);
//! assert_eq!(body, "/:key called");
//!
//! // Little helper function to make calling a service easier. Just for
//! // demonstration purposes.
//! async fn call_service<S>(
//! svc: &mut S,
//! method: Method,
//! uri: &str,
//! ) -> (StatusCode, String)
//! where
//! S: Service<Request<Body>, Response = Response<BoxBody>, Error = Infallible>
//! {
//! let req = Request::builder().method(method).uri(uri).body(Body::empty()).unwrap();
//! let res = svc.ready().await.unwrap().call(req).await.unwrap();
//!
//! let status = res.status();
//!
//! let body = res.into_body();
//! let body = hyper::body::to_bytes(body).await.unwrap();
//! let body = String::from_utf8(body.to_vec()).unwrap();
//!
//! (status, body)
//! }
//! # }
//! ```
//! Routes are not allowed to overlap and will panic if an overlapping route is
//! added. This also means the order in which routes are added doesn't matter.
//!
//! ## Routing to any [`Service`]
//!
Expand Down Expand Up @@ -376,6 +309,41 @@
//! See ["Error handling"](#error-handling) for more details on [`handle_error`]
//! and error handling in general.
//!
//! ## Wildcard routes
//!
//! axum also supports wildcard routes:
//!
//! ```rust,no_run
//! use axum::{
//! handler::get,
//! Router,
//! };
//!
//! let app = Router::new()
//! // this matches any request that starts with `/api`
//! .route("/api/*rest", get(|| async { /* ... */ }));
//! # async {
//! # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
//! # };
//! ```
//!
//! The matched path can be extracted via [`extract::Path`]:
//!
//! ```rust,no_run
//! use axum::{
//! handler::get,
//! extract::Path,
//! Router,
//! };
//!
//! let app = Router::new().route("/api/*rest", get(|Path(rest): Path<String>| async {
//! // `rest` will be everything after `/api`
//! }));
//! # async {
//! # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
//! # };
//! ```
//!
//! ## Nesting routes
//!
//! Routes can be nested by calling [`Router::nest`](routing::Router::nest):
Expand Down Expand Up @@ -410,6 +378,25 @@
//! file serving to work. Use [`OriginalUri`] if you need the original request
//! URI.
//!
//! Nested routes are similar to wild card routes. The difference is that
//! wildcard routes still see the whole URI whereas nested routes will have
//! the prefix stripped.
//!
//! ```rust
//! use axum::{handler::get, http::Uri, Router};
//!
//! let app = Router::new()
//! .route("/foo/*rest", get(|uri: Uri| async {
//! // `uri` will contain `/foo`
//! }))
//! .nest("/bar", get(|uri: Uri| async {
//! // `uri` will _not_ contain `/bar`
//! }));
//! # async {
//! # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
//! # };
//! ```
//!
//! # Extractors
//!
//! An extractor is a type that implements [`FromRequest`]. Extractors is how
Expand Down
62 changes: 25 additions & 37 deletions src/routing/future.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
//! Future types.

use crate::{
body::BoxBody, clone_box_service::CloneBoxService, routing::FromEmptyRouter, BoxError,
body::BoxBody,
clone_box_service::CloneBoxService,
routing::{FromEmptyRouter, UriStack},
BoxError,
};
use futures_util::ready;
use http::{Request, Response};
use pin_project_lite::pin_project;
use std::{
Expand All @@ -13,7 +15,7 @@ use std::{
pin::Pin,
task::{Context, Poll},
};
use tower::{util::Oneshot, ServiceExt};
use tower::util::Oneshot;
use tower_service::Service;

pub use super::or::ResponseFuture as OrResponseFuture;
Expand Down Expand Up @@ -76,12 +78,9 @@ where
S: Service<Request<B>>,
F: Service<Request<B>>,
{
pub(crate) fn a(a: Oneshot<S, Request<B>>, fallback: F) -> Self {
pub(crate) fn a(a: Oneshot<S, Request<B>>) -> Self {
RouteFuture {
state: RouteFutureInner::A {
a,
fallback: Some(fallback),
},
state: RouteFutureInner::A { a },
}
}

Expand All @@ -103,7 +102,6 @@ pin_project! {
A {
#[pin]
a: Oneshot<S, Request<B>>,
fallback: Option<F>,
},
B {
#[pin]
Expand All @@ -120,33 +118,10 @@ where
{
type Output = Result<Response<BoxBody>, S::Error>;

fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
loop {
let mut this = self.as_mut().project();

let new_state = match this.state.as_mut().project() {
RouteFutureInnerProj::A { a, fallback } => {
let mut response = ready!(a.poll(cx))?;

let req = if let Some(ext) =
response.extensions_mut().remove::<FromEmptyRouter<B>>()
{
ext.request
} else {
return Poll::Ready(Ok(response));
};

RouteFutureInner::B {
b: fallback
.take()
.expect("future polled after completion")
.oneshot(req),
}
}
RouteFutureInnerProj::B { b } => return b.poll(cx),
};

this.state.set(new_state);
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project().state.project() {
RouteFutureInnerProj::A { a } => a.poll(cx),
RouteFutureInnerProj::B { b } => b.poll(cx),
}
}
}
Expand All @@ -173,7 +148,20 @@ where
type Output = Result<Response<BoxBody>, S::Error>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project().inner.poll(cx)
let mut res: Response<_> = futures_util::ready!(self.project().inner.poll(cx)?);

// `nest` mutates the URI of the request so if it turns out no route matched
// we need to reset the URI so the next routes see the original URI
//
// That requires using a stack since we can have arbitrarily nested routes
if let Some(from_empty_router) = res.extensions_mut().get_mut::<FromEmptyRouter<B>>() {
let uri = UriStack::pop(&mut from_empty_router.request);
if let Some(uri) = uri {
*from_empty_router.request.uri_mut() = uri;
}
}

Poll::Ready(Ok(res))
}
}

Expand Down
Loading

0 comments on commit 1a78a3f

Please sign in to comment.