From 21c96e0aa18008922634824fe5b58feba5254265 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 6 Jun 2021 23:58:44 +0200 Subject: [PATCH] Write a few docs --- examples/key_value_store.rs | 2 +- src/body.rs | 4 + src/handler.rs | 142 ++++++++++++++++++++++++++++++++---- src/lib.rs | 106 +++++++++++++++++++++++---- src/macros.rs | 34 +++++++++ src/response.rs | 31 ++++++++ src/routing.rs | 90 +++++++++++++++-------- src/service.rs | 102 +++++++++++++++++++++++--- src/tests.rs | 2 +- 9 files changed, 441 insertions(+), 72 deletions(-) create mode 100644 src/macros.rs diff --git a/examples/key_value_store.rs b/examples/key_value_store.rs index f9052874..2224028c 100644 --- a/examples/key_value_store.rs +++ b/examples/key_value_store.rs @@ -14,7 +14,7 @@ use tower_http::{ use tower_web::{ body::Body, extract::{BytesMaxLength, Extension, UrlParams}, - get, route, Handler, + prelude::*, }; #[tokio::main] diff --git a/src/body.rs b/src/body.rs index d629f30d..a25f79eb 100644 --- a/src/body.rs +++ b/src/body.rs @@ -77,6 +77,10 @@ where } } +/// A boxed error trait object that implements [`std::error::Error`]. +/// +/// This is necessary for compatibility with middleware that changes the error +/// type of the response body. // work around for `BoxError` not implementing `std::error::Error` // // This is currently required since tower-http's Compression middleware's body type's diff --git a/src/handler.rs b/src/handler.rs index 0865800f..8ba99047 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -2,7 +2,7 @@ use crate::{ body::{Body, BoxBody}, extract::FromRequest, response::IntoResponse, - routing::{BoxResponseBody, EmptyRouter, MethodFilter}, + routing::{BoxResponseBody, EmptyRouter, MethodFilter, RouteFuture}, service::HandleError, }; use async_trait::async_trait; @@ -15,7 +15,28 @@ use std::{ marker::PhantomData, task::{Context, Poll}, }; -use tower::{util::Oneshot, BoxError, Layer, Service, ServiceExt}; +use tower::{BoxError, Layer, Service, ServiceExt}; + +pub fn any(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Any, handler) +} + +pub fn connect(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Connect, handler) +} + +pub fn delete(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Delete, handler) +} pub fn get(handler: H) -> OnMethod, EmptyRouter> where @@ -24,6 +45,27 @@ where on(MethodFilter::Get, handler) } +pub fn head(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Head, handler) +} + +pub fn options(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Options, handler) +} + +pub fn patch(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Patch, handler) +} + pub fn post(handler: H) -> OnMethod, EmptyRouter> where H: Handler, @@ -31,6 +73,20 @@ where on(MethodFilter::Post, handler) } +pub fn put(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Put, handler) +} + +pub fn trace(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Trace, handler) +} + pub fn on(method: MethodFilter, handler: H) -> OnMethod, EmptyRouter> where H: Handler, @@ -216,7 +272,7 @@ where { type Response = Response; type Error = Infallible; - type Future = future::BoxFuture<'static, Result>; + type Future = IntoServiceFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { // `IntoService` can only be constructed from async functions which are always ready, or from @@ -227,13 +283,19 @@ where fn call(&mut self, req: Request) -> Self::Future { let handler = self.handler.clone(); - Box::pin(async move { + let future = Box::pin(async move { let res = Handler::call(handler, req).await; Ok(res) - }) + }); + IntoServiceFuture(future) } } +opaque_future! { + pub type IntoServiceFuture = + future::BoxFuture<'static, Result, Infallible>>; +} + #[derive(Clone)] pub struct OnMethod { pub(crate) method: MethodFilter, @@ -242,6 +304,27 @@ pub struct OnMethod { } impl OnMethod { + pub fn any(self, handler: H) -> OnMethod, Self> + where + H: Handler, + { + self.on(MethodFilter::Any, handler) + } + + pub fn connect(self, handler: H) -> OnMethod, Self> + where + H: Handler, + { + self.on(MethodFilter::Connect, handler) + } + + pub fn delete(self, handler: H) -> OnMethod, Self> + where + H: Handler, + { + self.on(MethodFilter::Delete, handler) + } + pub fn get(self, handler: H) -> OnMethod, Self> where H: Handler, @@ -249,6 +332,27 @@ impl OnMethod { self.on(MethodFilter::Get, handler) } + pub fn head(self, handler: H) -> OnMethod, Self> + where + H: Handler, + { + self.on(MethodFilter::Head, handler) + } + + pub fn options(self, handler: H) -> OnMethod, Self> + where + H: Handler, + { + self.on(MethodFilter::Options, handler) + } + + pub fn patch(self, handler: H) -> OnMethod, Self> + where + H: Handler, + { + self.on(MethodFilter::Patch, handler) + } + pub fn post(self, handler: H) -> OnMethod, Self> where H: Handler, @@ -256,6 +360,20 @@ impl OnMethod { self.on(MethodFilter::Post, handler) } + pub fn put(self, handler: H) -> OnMethod, Self> + where + H: Handler, + { + self.on(MethodFilter::Put, handler) + } + + pub fn trace(self, handler: H) -> OnMethod, Self> + where + H: Handler, + { + self.on(MethodFilter::Trace, handler) + } + pub fn on(self, method: MethodFilter, handler: H) -> OnMethod, Self> where H: Handler, @@ -268,8 +386,6 @@ impl OnMethod { } } -// this is identical to `routing::OnMethod`'s implementation. Would be nice to find a way to clean -// that up, but not sure its possible. impl Service> for OnMethod where S: Service, Response = Response, Error = Infallible> + Clone, @@ -282,24 +398,20 @@ where { type Response = Response; type Error = Infallible; - - #[allow(clippy::type_complexity)] - type Future = future::Either< - BoxResponseBody>>, - BoxResponseBody>>, - >; + type Future = RouteFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: Request) -> Self::Future { - if self.method.matches(req.method()) { + let f = if self.method.matches(req.method()) { 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)) - } + }; + RouteFuture(f) } } diff --git a/src/lib.rs b/src/lib.rs index 41d820f3..f0360878 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -500,7 +500,6 @@ //! use tower_http::services::ServeFile; //! use http::Response; //! use std::convert::Infallible; -//! use tower::{service_fn, BoxError}; //! //! fn api_routes() -> BoxRoute { //! route("/users", get(|_: Request| async { /* ... */ })).boxed() @@ -571,7 +570,7 @@ rust_2018_idioms, future_incompatible, nonstandard_style, - // missing_docs + missing_docs )] #![deny(unreachable_pub, broken_intra_doc_links, private_in_public)] #![allow( @@ -592,6 +591,9 @@ use routing::{EmptyRouter, Route}; use std::convert::Infallible; use tower::{BoxError, Service}; +#[macro_use] +pub(crate) mod macros; + pub mod body; pub mod extract; pub mod handler; @@ -602,12 +604,6 @@ pub mod service; #[cfg(test)] mod tests; -#[doc(inline)] -pub use self::{ - handler::{get, on, post, Handler}, - routing::RoutingDsl, -}; - pub use async_trait::async_trait; pub use tower_http::add_extension::{AddExtension, AddExtensionLayer}; @@ -615,24 +611,102 @@ pub mod prelude { //! Re-exports of important traits, types, and functions used with tower-web. Meant to be glob //! imported. - pub use crate::{ - body::Body, - extract, - handler::{get, on, post, Handler}, - response, route, - routing::RoutingDsl, + pub use crate::body::Body; + pub use crate::extract; + pub use crate::handler::{ + any, connect, delete, get, head, options, patch, post, put, trace, Handler, }; + pub use crate::response; + pub use crate::route; + pub use crate::routing::RoutingDsl; pub use http::Request; } -pub fn route(spec: &str, svc: S) -> Route +/// Create a route. +/// +/// `description` is a string of path segments separated by `/`. Each segment +/// can be either concrete or a capture: +/// +/// - `/foo/bar/baz` will only match requests where the path is `/foo/bar/bar`. +/// - `/:foo` will match any route with exactly one segment _and_ it will +/// capture the first segment and store it only the key `foo`. +/// +/// `service` is the [`Service`] that should receive the request if the path +/// matches `description`. +/// +/// Note that `service`'s error type must be [`Infallible`] meaning you must +/// handle all errors. If you're creating handlers from async functions that is +/// handled automatically but if you're routing to some other [`Service`] you +/// might need to use [`handle_error`](ServiceExt::handle_error) to map errors +/// into responses. +/// +/// # Examples +/// +/// ```rust +/// use tower_web::prelude::*; +/// # use std::convert::Infallible; +/// # use http::Response; +/// # let service = tower::service_fn(|_: Request| async { +/// # Ok::, Infallible>(Response::new(Body::empty())) +/// # }); +/// +/// route("/", service); +/// route("/users", service); +/// route("/users/:id", service); +/// route("/api/:version/users/:id/action", service); +/// ``` +/// +/// # Panics +/// +/// Panics if `description` doesn't start with `/`. +pub fn route(description: &str, service: S) -> Route where S: Service, Error = Infallible> + Clone, { - routing::EmptyRouter.route(spec, svc) + use routing::RoutingDsl; + + routing::EmptyRouter.route(description, service) } +/// Extension trait that adds additional methods to [`Service`]. pub trait ServiceExt: Service, Response = Response> { + /// Handle errors from a service. + /// + /// tower-web requires all handles to never return errors. If you route to + /// [`Service`], not created by tower-web, who's error isn't `Infallible` + /// you can use this combinator to handle the error. + /// + /// `handle_error` takes a closure that will map errors from the service + /// into responses. The closure's return type must implement + /// [`IntoResponse`]. + /// + /// # Example + /// + /// ```rust,no_run + /// use tower_web::{ + /// service, prelude::*, + /// ServiceExt, + /// }; + /// use http::Response; + /// use tower::{service_fn, BoxError}; + /// + /// // A service that might fail with `std::io::Error` + /// let service = service_fn(|_: Request| async { + /// let res = Response::new(Body::empty()); + /// Ok::<_, std::io::Error>(res) + /// }); + /// + /// let app = route( + /// "/", + /// service.handle_error(|error: std::io::Error| { + /// // Handle error by returning something that implements `IntoResponse` + /// }), + /// ); + /// # + /// # async { + /// # hyper::Server::bind(&"".parse().unwrap()).serve(tower::make::Shared::new(app)).await; + /// # }; + /// ``` fn handle_error(self, f: F) -> service::HandleError where Self: Sized, diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 00000000..87a71ceb --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,34 @@ +//! Internal macros + +macro_rules! opaque_future { + ($(#[$m:meta])* pub type $name:ident = $actual:ty;) => { + opaque_future! { + $(#[$m])* + pub type $name<> = $actual; + } + }; + + ($(#[$m:meta])* pub type $name:ident<$($param:ident),*> = $actual:ty;) => { + #[pin_project::pin_project] + $(#[$m])* + pub struct $name<$($param),*>(#[pin] pub(crate) $actual) + where; + + impl<$($param),*> std::fmt::Debug for $name<$($param),*> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple(stringify!($name)).field(&format_args!("...")).finish() + } + } + + impl<$($param),*> std::future::Future for $name<$($param),*> + where + $actual: std::future::Future, + { + type Output = <$actual as std::future::Future>::Output; + #[inline] + fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll { + self.project().0.poll(cx) + } + } + }; +} diff --git a/src/response.rs b/src/response.rs index 6539cad2..d7331465 100644 --- a/src/response.rs +++ b/src/response.rs @@ -5,7 +5,11 @@ use serde::Serialize; use std::{borrow::Cow, convert::Infallible}; use tower::util::Either; +/// Trait for generating responses. +/// +/// Types that implement `IntoResponse` can be returned from handlers. pub trait IntoResponse { + /// Create a response. fn into_response(self) -> Response; } @@ -153,6 +157,9 @@ where } } +/// An HTML response. +/// +/// Will automatically get `Content-Type: text/html`. pub struct Html(pub T); impl IntoResponse for Html @@ -167,6 +174,30 @@ where } } +/// A JSON response. +/// +/// Can be created from any type that implements [`serde::Serialize`]. +/// +/// Will automatically get `Content-Type: application/json`. +/// +/// # Example +/// +/// ``` +/// use serde_json::json; +/// use tower_web::{body::Body, response::{Json, IntoResponse}}; +/// use http::{Response, header::CONTENT_TYPE}; +/// +/// let json = json!({ +/// "data": 42, +/// }); +/// +/// let response: Response = Json(json).into_response(); +/// +/// assert_eq!( +/// response.headers().get(CONTENT_TYPE).unwrap(), +/// "application/json", +/// ); +/// ``` pub struct Json(pub T); impl IntoResponse for Json diff --git a/src/routing.rs b/src/routing.rs index 4ca3c965..b6735f68 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -64,23 +64,23 @@ pub struct Route { } pub trait RoutingDsl: Sized { - fn route(self, spec: &str, svc: T) -> Route + fn route(self, description: &str, svc: T) -> Route where T: Service, Error = Infallible> + Clone, { Route { - pattern: PathPattern::new(spec), + pattern: PathPattern::new(description), svc, fallback: self, } } - fn nest(self, spec: &str, svc: T) -> Nested + fn nest(self, description: &str, svc: T) -> Nested where T: Service, Error = Infallible> + Clone, { Nested { - pattern: PathPattern::new(spec), + pattern: PathPattern::new(description), svc, fallback: self, } @@ -125,26 +125,50 @@ where { type Response = Response; type Error = Infallible; - - #[allow(clippy::type_complexity)] - type Future = future::Either< - BoxResponseBody>>, - BoxResponseBody>>, - >; + type Future = RouteFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, mut req: Request) -> Self::Future { - if let Some(captures) = self.pattern.full_match(req.uri().path()) { + let f = 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)) } else { let response_future = self.fallback.clone().oneshot(req); future::Either::Right(BoxResponseBody(response_future)) - } + }; + RouteFuture(f) + } +} + +#[pin_project] +pub struct RouteFuture( + #[pin] + pub(crate) future::Either< + BoxResponseBody>>, + BoxResponseBody>>, + >, +) +where + S: Service>, + F: Service>; + +impl Future for RouteFuture +where + S: Service, Response = Response, Error = Infallible>, + SB: http_body::Body + Send + Sync + 'static, + SB::Error: Into, + F: Service, Response = Response, Error = Infallible>, + FB: http_body::Body + Send + Sync + 'static, + FB::Error: Into, +{ + type Output = Result, Infallible>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project().0.poll(cx) } } @@ -187,22 +211,27 @@ pub struct EmptyRouter; impl RoutingDsl for EmptyRouter {} -impl Service for EmptyRouter { +impl Service> for EmptyRouter { type Response = Response; type Error = Infallible; - type Future = future::Ready>; + type Future = EmptyRouterFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } - fn call(&mut self, _req: R) -> Self::Future { + fn call(&mut self, _req: Request) -> Self::Future { let mut res = Response::new(Body::empty()); *res.status_mut() = StatusCode::NOT_FOUND; - future::ok(res) + EmptyRouterFuture(future::ok(res)) } } +opaque_future! { + pub type EmptyRouterFuture = + future::Ready, Infallible>>; +} + // ===== PathPattern ===== #[derive(Debug, Clone)] @@ -216,6 +245,11 @@ struct Inner { impl PathPattern { pub(crate) fn new(pattern: &str) -> Self { + assert!( + pattern.starts_with('/'), + "Route description must start with a `/`" + ); + let mut capture_group_names = Vec::new(); let pattern = pattern @@ -309,7 +343,7 @@ where { type Response = Response; type Error = Infallible; - type Future = BoxRouteResponseFuture; + type Future = BoxRouteFuture; #[inline] fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { @@ -318,19 +352,19 @@ where #[inline] fn call(&mut self, req: Request) -> Self::Future { - BoxRouteResponseFuture(self.0.clone().oneshot(req)) + BoxRouteFuture(self.0.clone().oneshot(req)) } } #[pin_project] -pub struct BoxRouteResponseFuture(#[pin] InnerFuture); +pub struct BoxRouteFuture(#[pin] InnerFuture); type InnerFuture = Oneshot< Buffer, Response, Infallible>, Request>, Request, >; -impl Future for BoxRouteResponseFuture +impl Future for BoxRouteFuture where B: http_body::Body + Send + Sync + 'static, B::Error: Into + Send + Sync + 'static, @@ -418,12 +452,12 @@ where // ===== nesting ===== -pub fn nest(spec: &str, svc: S) -> Nested +pub fn nest(description: &str, svc: S) -> Nested where S: Service, Error = Infallible> + Clone, { Nested { - pattern: PathPattern::new(spec), + pattern: PathPattern::new(description), svc, fallback: EmptyRouter, } @@ -450,19 +484,14 @@ where { type Response = Response; type Error = Infallible; - - #[allow(clippy::type_complexity)] - type Future = future::Either< - BoxResponseBody>>, - BoxResponseBody>>, - >; + type Future = RouteFuture; 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 f = 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; @@ -472,7 +501,8 @@ where } else { let response_future = self.fallback.clone().oneshot(req); future::Either::Right(BoxResponseBody(response_future)) - } + }; + RouteFuture(f) } } diff --git a/src/service.rs b/src/service.rs index 6c129c40..927451a2 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,7 +1,7 @@ use crate::{ body::{Body, BoxBody}, response::IntoResponse, - routing::{BoxResponseBody, EmptyRouter, MethodFilter}, + routing::{BoxResponseBody, EmptyRouter, MethodFilter, RouteFuture}, }; use bytes::Bytes; use futures_util::future; @@ -17,14 +17,46 @@ use std::{ }; use tower::{util::Oneshot, BoxError, Service, ServiceExt as _}; +pub fn any(svc: S) -> OnMethod { + on(MethodFilter::Any, svc) +} + +pub fn connect(svc: S) -> OnMethod { + on(MethodFilter::Connect, svc) +} + +pub fn delete(svc: S) -> OnMethod { + on(MethodFilter::Delete, svc) +} + pub fn get(svc: S) -> OnMethod { on(MethodFilter::Get, svc) } +pub fn head(svc: S) -> OnMethod { + on(MethodFilter::Head, svc) +} + +pub fn options(svc: S) -> OnMethod { + on(MethodFilter::Options, svc) +} + +pub fn patch(svc: S) -> OnMethod { + on(MethodFilter::Patch, svc) +} + pub fn post(svc: S) -> OnMethod { on(MethodFilter::Post, svc) } +pub fn put(svc: S) -> OnMethod { + on(MethodFilter::Put, svc) +} + +pub fn trace(svc: S) -> OnMethod { + on(MethodFilter::Trace, svc) +} + pub fn on(method: MethodFilter, svc: S) -> OnMethod { OnMethod { method, @@ -41,6 +73,27 @@ pub struct OnMethod { } impl OnMethod { + pub fn any(self, svc: T) -> OnMethod + where + T: Service> + Clone, + { + self.on(MethodFilter::Any, svc) + } + + pub fn connect(self, svc: T) -> OnMethod + where + T: Service> + Clone, + { + self.on(MethodFilter::Connect, svc) + } + + pub fn delete(self, svc: T) -> OnMethod + where + T: Service> + Clone, + { + self.on(MethodFilter::Delete, svc) + } + pub fn get(self, svc: T) -> OnMethod where T: Service> + Clone, @@ -48,6 +101,27 @@ impl OnMethod { self.on(MethodFilter::Get, svc) } + pub fn head(self, svc: T) -> OnMethod + where + T: Service> + Clone, + { + self.on(MethodFilter::Head, svc) + } + + pub fn options(self, svc: T) -> OnMethod + where + T: Service> + Clone, + { + self.on(MethodFilter::Options, svc) + } + + pub fn patch(self, svc: T) -> OnMethod + where + T: Service> + Clone, + { + self.on(MethodFilter::Patch, svc) + } + pub fn post(self, svc: T) -> OnMethod where T: Service> + Clone, @@ -55,6 +129,20 @@ impl OnMethod { self.on(MethodFilter::Post, svc) } + pub fn put(self, svc: T) -> OnMethod + where + T: Service> + Clone, + { + self.on(MethodFilter::Put, svc) + } + + pub fn trace(self, svc: T) -> OnMethod + where + T: Service> + Clone, + { + self.on(MethodFilter::Trace, svc) + } + pub fn on(self, method: MethodFilter, svc: T) -> OnMethod where T: Service> + Clone, @@ -81,25 +169,21 @@ where { type Response = Response; type Error = Infallible; - - #[allow(clippy::type_complexity)] - type Future = future::Either< - BoxResponseBody>>, - BoxResponseBody>>, - >; + type Future = RouteFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: Request) -> Self::Future { - if self.method.matches(req.method()) { + let f = if self.method.matches(req.method()) { 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)) - } + }; + RouteFuture(f) } } diff --git a/src/tests.rs b/src/tests.rs index 03165a62..d0b3207f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,4 +1,4 @@ -use crate::{extract, get, on, post, route, routing::MethodFilter, service, Handler, RoutingDsl}; +use crate::{handler::on, prelude::*, routing::MethodFilter, service}; use http::{Request, Response, StatusCode}; use hyper::{Body, Server}; use serde::Deserialize;