From 1609191a74f4f105400f80f8c2bad887c1f060cf Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 6 Jun 2021 20:30:54 +0200 Subject: [PATCH] Nesting and more flexible routing dsl --- src/lib.rs | 4 +- src/routing.rs | 276 +++++++++++++++++++++++++++++++------------------ src/tests.rs | 203 ++++++++++++------------------------ 3 files changed, 245 insertions(+), 238 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e179f879..3664a01e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -569,7 +569,7 @@ pub mod service; #[doc(inline)] pub use self::{ handler::{get, on, post, Handler}, - routing::AddRoute, + routing::RoutingDsl, }; pub use async_trait::async_trait; @@ -581,7 +581,7 @@ pub mod prelude { extract, handler::{get, on, post, Handler}, response, route, - routing::AddRoute, + routing::RoutingDsl, }; pub use http::Request; } diff --git a/src/routing.rs b/src/routing.rs index 5dfefd91..15479a82 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -1,7 +1,8 @@ use crate::{body::BoxBody, response::IntoResponse, ResultExt}; use bytes::Bytes; use futures_util::{future, ready}; -use http::{Method, Request, Response, StatusCode}; +use http::{Method, Request, Response, StatusCode, Uri}; +use http_body::Full; use hyper::Body; use itertools::Itertools; use pin_project::pin_project; @@ -62,36 +63,7 @@ pub struct Route { pub(crate) fallback: F, } -pub trait AddRoute: Sized { - fn route(self, spec: &str, svc: T) -> Route - where - T: Service, Error = Infallible> + Clone; -} - -impl Route { - pub fn boxed(self) -> BoxRoute - where - Self: Service, Response = Response, Error = Infallible> + Send + 'static, - >>::Future: Send, - B: From + 'static, - { - ServiceBuilder::new() - .layer_fn(BoxRoute) - .buffer(1024) - .layer(BoxService::layer()) - .service(self) - } - - pub fn layer(self, layer: L) -> Layered - where - L: Layer, - L::Service: Service> + Clone, - { - Layered(layer.layer(self)) - } -} - -impl AddRoute for Route { +pub trait RoutingDsl: Sized { fn route(self, spec: &str, svc: T) -> Route where T: Service, Error = Infallible> + Clone, @@ -102,8 +74,43 @@ impl AddRoute for Route { fallback: self, } } + + fn nest(self, spec: &str, svc: T) -> Nested + where + T: Service, Error = Infallible> + Clone, + { + Nested { + pattern: PathPattern::new(spec), + svc, + fallback: self, + } + } + + fn boxed(self) -> BoxRoute + where + Self: Service, Response = Response, Error = Infallible> + Send + 'static, + >>::Future: Send, + B: http_body::Body + Send + Sync + 'static, + B::Error: Into + Send + Sync + 'static, + { + ServiceBuilder::new() + .layer_fn(BoxRoute) + .buffer(1024) + .layer(BoxService::layer()) + .service(self) + } + + fn layer(self, layer: L) -> Layered + where + L: Layer, + L::Service: Service> + Clone, + { + Layered(layer.layer(self)) + } } +impl RoutingDsl for Route {} + // ===== Routing service impls ===== impl Service> for Route @@ -130,7 +137,7 @@ where } fn call(&mut self, mut req: Request) -> Self::Future { - if let Some(captures) = self.pattern.matches(req.uri().path()) { + if let Some(captures) = self.pattern.full_match(req.uri().path()) { insert_url_params(&mut req, captures); let response_future = self.svc.clone().oneshot(req); future::Either::Left(BoxResponseBody(response_future)) @@ -178,18 +185,7 @@ where #[derive(Clone, Copy)] pub struct EmptyRouter; -impl AddRoute for EmptyRouter { - fn route(self, spec: &str, svc: S) -> Route - where - S: Service, Error = Infallible> + Clone, - { - Route { - pattern: PathPattern::new(spec), - svc, - fallback: self, - } - } -} +impl RoutingDsl for EmptyRouter {} impl Service for EmptyRouter { type Response = Response; @@ -236,7 +232,7 @@ impl PathPattern { .join("/"); let full_path_regex = - Regex::new(&format!("^{}$", pattern)).expect("invalid regex generated from route"); + Regex::new(&format!("^{}", pattern)).expect("invalid regex generated from route"); Self(Arc::new(Inner { full_path_regex, @@ -244,8 +240,26 @@ impl PathPattern { })) } - pub(crate) fn matches(&self, path: &str) -> Option { + pub(crate) fn full_match(&self, path: &str) -> Option { + self.do_match(path).and_then(|match_| { + if match_.full_match { + Some(match_.captures) + } else { + None + } + }) + } + + pub(crate) fn prefix_match<'a>(&self, path: &'a str) -> Option<(&'a str, Captures)> { + self.do_match(path) + .map(|match_| (match_.matched, match_.captures)) + } + + fn do_match<'a>(&self, path: &'a str) -> Option> { self.0.full_path_regex.captures(path).map(|captures| { + let matched = captures.get(0).unwrap(); + let full_match = matched.as_str() == path; + let captures = self .0 .capture_group_names @@ -258,11 +272,22 @@ impl PathPattern { .map(|(key, value)| (key.to_string(), value.to_string())) .collect::>(); - captures + Match { + captures, + full_match, + matched: matched.as_str(), + } }) } } +struct Match<'a> { + captures: Captures, + // true if regex matched whole path, false if it only matched a prefix + full_match: bool, + matched: &'a str, +} + type Captures = Vec<(String, String)>; // ===== BoxRoute ===== @@ -275,24 +300,14 @@ impl Clone for BoxRoute { } } -impl AddRoute for BoxRoute { - fn route(self, spec: &str, svc: S) -> Route - where - S: Service, Error = Infallible> + Clone, - { - Route { - pattern: PathPattern::new(spec), - svc, - fallback: self, - } - } -} +impl RoutingDsl for BoxRoute {} impl Service> for BoxRoute where - B: From + 'static, + B: http_body::Body + Send + Sync + 'static, + B::Error: Into + Send + Sync + 'static, { - type Response = Response; + type Response = Response; type Error = Infallible; type Future = BoxRouteResponseFuture; @@ -317,29 +332,27 @@ type InnerFuture = Oneshot< impl Future for BoxRouteResponseFuture where - B: From, + B: http_body::Body + Send + Sync + 'static, + B::Error: Into + Send + Sync + 'static, { - type Output = Result, Infallible>; + type Output = Result, Infallible>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match ready!(self.project().0.poll(cx)) { - Ok(res) => Poll::Ready(Ok(res)), + Ok(res) => Poll::Ready(Ok(res.map(BoxBody::new))), Err(err) => Poll::Ready(Ok(handle_buffer_error(err))), } } } -fn handle_buffer_error(error: BoxError) -> Response -where - B: From, -{ +fn handle_buffer_error(error: BoxError) -> Response { use tower::buffer::error::{Closed, ServiceError}; let error = match error.downcast::() { Ok(closed) => { return Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(B::from(closed.to_string())) + .body(BoxBody::new(Full::from(closed.to_string()))) .unwrap(); } Err(e) => e, @@ -349,7 +362,7 @@ where Ok(service_error) => { return Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(B::from(format!("Service error: {}. This is a bug in tower-web. All inner services should be infallible. Please file an issue", service_error))) + .body(BoxBody::new(Full::from(format!("Service error: {}. This is a bug in tower-web. All inner services should be infallible. Please file an issue", service_error)))) .unwrap(); } Err(e) => e, @@ -357,10 +370,10 @@ where Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(B::from(format!( + .body(BoxBody::new(Full::from(format!( "Uncountered an unknown error: {}. This should never happen. Please file an issue", error - ))) + )))) .unwrap() } @@ -369,18 +382,7 @@ where #[derive(Clone, Debug)] pub struct Layered(S); -impl AddRoute for Layered { - fn route(self, spec: &str, svc: T) -> Route - where - T: Service, Error = Infallible> + Clone, - { - Route { - pattern: PathPattern::new(spec), - svc, - fallback: self, - } - } -} +impl RoutingDsl for Layered {} impl Layered { pub fn handle_error(self, f: F) -> HandleError @@ -420,18 +422,7 @@ pub struct HandleError { f: F, } -impl AddRoute for HandleError { - fn route(self, spec: &str, svc: T) -> Route - where - T: Service, Error = Infallible> + Clone, - { - Route { - pattern: PathPattern::new(spec), - svc, - fallback: self, - } - } -} +impl RoutingDsl for HandleError {} impl Service> for HandleError where @@ -487,6 +478,95 @@ where } } +// ===== nesting ===== + +pub fn nest(spec: &str, svc: S) -> Nested +where + S: Service, Error = Infallible> + Clone, +{ + Nested { + pattern: PathPattern::new(spec), + svc, + fallback: EmptyRouter, + } +} + +#[derive(Debug, Clone)] +pub struct Nested { + pattern: PathPattern, + svc: S, + fallback: F, +} + +impl RoutingDsl for Nested {} + +impl Service> for Nested +where + S: Service, Response = Response, Error = Infallible> + Clone, + SB: http_body::Body + Send + Sync + 'static, + SB::Error: Into, + + F: Service, Response = Response, Error = Infallible> + Clone, + FB: http_body::Body + Send + Sync + 'static, + FB::Error: Into, +{ + type Response = Response; + type Error = Infallible; + + #[allow(clippy::type_complexity)] + type Future = future::Either< + BoxResponseBody>>, + BoxResponseBody>>, + >; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + if let Some((prefix, captures)) = self.pattern.prefix_match(req.uri().path()) { + let without_prefix = strip_prefix(req.uri(), prefix); + *req.uri_mut() = without_prefix; + + insert_url_params(&mut req, captures); + let response_future = self.svc.clone().oneshot(req); + future::Either::Left(BoxResponseBody(response_future)) + } else { + let response_future = self.fallback.clone().oneshot(req); + future::Either::Right(BoxResponseBody(response_future)) + } + } +} + +fn strip_prefix(uri: &Uri, prefix: &str) -> Uri { + let path_and_query = if let Some(path_and_query) = uri.path_and_query() { + let new_path = if let Some(path) = path_and_query.path().strip_prefix(prefix) { + path + } else { + path_and_query.path() + }; + + if let Some(query) = path_and_query.query() { + Some( + format!("{}?{}", new_path, query) + .parse::() + .unwrap(), + ) + } else { + Some(new_path.parse().unwrap()) + } + } else { + None + }; + + let mut parts = http::uri::Parts::default(); + parts.scheme = uri.scheme().cloned(); + parts.authority = uri.authority().cloned(); + parts.path_and_query = path_and_query; + + Uri::from_parts(parts).unwrap() +} + #[cfg(test)] mod tests { use super::*; @@ -514,7 +594,7 @@ mod tests { fn assert_match(route_spec: &'static str, path: &'static str) { let route = PathPattern::new(route_spec); assert!( - route.matches(path).is_some(), + route.full_match(path).is_some(), "`{}` doesn't match `{}`", path, route_spec @@ -524,7 +604,7 @@ mod tests { fn refute_match(route_spec: &'static str, path: &'static str) { let route = PathPattern::new(route_spec); assert!( - route.matches(path).is_none(), + route.full_match(path).is_none(), "`{}` did match `{}` (but shouldn't)", path, route_spec diff --git a/src/tests.rs b/src/tests.rs index efb7effc..aa25edd4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,4 +1,4 @@ -use crate::{extract, get, on, post, route, routing::MethodFilter, service, AddRoute, Handler}; +use crate::{extract, get, on, post, route, routing::MethodFilter, service, Handler, RoutingDsl}; use http::{Request, Response, StatusCode}; use hyper::{Body, Server}; use serde::Deserialize; @@ -283,6 +283,7 @@ async fn boxing() { "hi from POST" }), ) + .layer(tower_http::compression::CompressionLayer::new()) .boxed(); let addr = run_in_background(app).await; @@ -485,150 +486,76 @@ async fn layer_on_whole_router() { assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); } -// TODO(david): layer that changes the response body type to have a different error +#[tokio::test] +async fn disjunction() { + let api_routes = route( + "/users", + get(|_: Request| async { "users#index" }) + .post(|_: Request| async { "users#create" }), + ) + .route( + "/users/:id", + get( + |_: Request, params: extract::UrlParamsMap| async move { + format!( + "{}: users#show ({})", + params.get("version").unwrap(), + params.get("id").unwrap() + ) + }, + ), + ) + .route( + "/games/:id", + get( + |_: Request, params: extract::UrlParamsMap| async move { + format!( + "{}: games#show ({})", + params.get("version").unwrap(), + params.get("id").unwrap() + ) + }, + ), + ); -// // #[tokio::test] -// // async fn nesting() { -// // let api = app() -// // .at("/users") -// // .get(|_: Request| async { "users#index" }) -// // .post(|_: Request| async { "users#create" }) -// // .at("/users/:id") -// // .get( -// // |_: Request, params: extract::UrlParams<(i32,)>| async move { -// // let (id,) = params.0; -// // format!("users#show {}", id) -// // }, -// // ); + let app = route("/", get(|_: Request| async { "hi" })).nest("/:version/api", api_routes); -// // let app = app() -// // .at("/foo") -// // .get(|_: Request| async { "foo" }) -// // .at("/api") -// // .nest(api) -// // .at("/bar") -// // .get(|_: Request| async { "bar" }) -// // .into_service(); + let addr = run_in_background(app).await; -// // let addr = run_in_background(app).await; + let client = reqwest::Client::new(); -// // let client = reqwest::Client::new(); + let res = client + .get(format!("http://{}/", addr)) + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.text().await.unwrap(), "hi"); -// // let res = client -// // .get(format!("http://{}/api/users", addr)) -// // .send() -// // .await -// // .unwrap(); -// // assert_eq!(res.text().await.unwrap(), "users#index"); + let res = client + .get(format!("http://{}/v0/api/users", addr)) + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.text().await.unwrap(), "users#index"); -// // let res = client -// // .post(format!("http://{}/api/users", addr)) -// // .send() -// // .await -// // .unwrap(); -// // assert_eq!(res.text().await.unwrap(), "users#create"); + let res = client + .get(format!("http://{}/v0/api/users/123", addr)) + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.text().await.unwrap(), "v0: users#show (123)"); -// // let res = client -// // .get(format!("http://{}/api/users/42", addr)) -// // .send() -// // .await -// // .unwrap(); -// // assert_eq!(res.text().await.unwrap(), "users#show 42"); - -// // let res = client -// // .get(format!("http://{}/foo", addr)) -// // .send() -// // .await -// // .unwrap(); -// // assert_eq!(res.text().await.unwrap(), "foo"); - -// // let res = client -// // .get(format!("http://{}/bar", addr)) -// // .send() -// // .await -// // .unwrap(); -// // assert_eq!(res.text().await.unwrap(), "bar"); -// // } - -// // #[tokio::test] -// // async fn nesting_with_dynamic_part() { -// // let api = app().at("/users/:id").get( -// // |_: Request, params: extract::UrlParamsMap| async move { -// // // let (version, id) = params.0; -// // dbg!(¶ms); -// // let version = params.get("version").unwrap(); -// // let id = params.get("id").unwrap(); -// // format!("users#show {} {}", version, id) -// // }, -// // ); - -// // let app = app().at("/:version/api").nest(api).into_service(); - -// // let addr = run_in_background(app).await; - -// // let client = reqwest::Client::new(); - -// // let res = client -// // .get(format!("http://{}/v0/api/users/123", addr)) -// // .send() -// // .await -// // .unwrap(); -// // let status = res.status(); -// // assert_eq!(res.text().await.unwrap(), "users#show v0 123"); -// // assert_eq!(status, StatusCode::OK); -// // } - -// // #[tokio::test] -// // async fn nesting_more_deeply() { -// // let users_api = app() -// // .at("/:id") -// // .get(|req: Request| async move { -// // dbg!(&req.uri().path()); -// // "users#show" -// // }); - -// // let games_api = app() -// // .at("/") -// // .post(|req: Request| async move { -// // dbg!(&req.uri().path()); -// // "games#create" -// // }); - -// // let api = app() -// // .at("/users") -// // .nest(users_api) -// // .at("/games") -// // .nest(games_api); - -// // let app = app().at("/:version/api").nest(api).into_service(); - -// // let addr = run_in_background(app).await; - -// // let client = reqwest::Client::new(); - -// // // let res = client -// // // .get(format!("http://{}/v0/api/users/123", addr)) -// // // .send() -// // // .await -// // // .unwrap(); -// // // assert_eq!(res.status(), StatusCode::OK); - -// // println!("============================"); - -// // let res = client -// // .post(format!("http://{}/v0/api/games", addr)) -// // .send() -// // .await -// // .unwrap(); -// // assert_eq!(res.status(), StatusCode::OK); -// // } - -// // TODO(david): nesting more deeply - -// // TODO(david): composing two apps -// // TODO(david): composing two apps with one at a "sub path" -// // TODO(david): composing two boxed apps -// // TODO(david): composing two apps that have had layers applied + let res = client + .get(format!("http://{}/v0/api/games/123", addr)) + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.text().await.unwrap(), "v0: games#show (123)"); +} /// Run a `tower::Service` in the background and get a URI for it. async fn run_in_background(svc: S) -> SocketAddr