From 46398afc724a6a6dc680db694888b9b22f251c5b Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 6 Jun 2021 11:37:08 +0200 Subject: [PATCH] Move things around a bit --- examples/hello_world.rs | 2 +- src/body.rs | 14 +- src/{extract.rs => extract/mod.rs} | 117 ++------------- src/extract/rejection.rs | 108 ++++++++++++++ src/handler.rs | 59 ++++++-- src/lib.rs | 224 ++++++++--------------------- src/response.rs | 95 ++++++------ src/routing.rs | 53 +++++-- src/service.rs | 112 +++++++++++++++ src/tests.rs | 8 +- 10 files changed, 446 insertions(+), 346 deletions(-) rename src/{extract.rs => extract/mod.rs} (74%) create mode 100644 src/extract/rejection.rs create mode 100644 src/service.rs diff --git a/examples/hello_world.rs b/examples/hello_world.rs index cec31a14..9aefe148 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -2,7 +2,7 @@ use http::{Request, StatusCode}; use hyper::Server; use std::net::SocketAddr; use tower::make::Shared; -use tower_web::{body::Body, response, get, route, AddRoute, extract}; +use tower_web::{body::Body, extract, get, response, route, AddRoute}; #[tokio::main] async fn main() { diff --git a/src/body.rs b/src/body.rs index b03f7c1c..d629f30d 100644 --- a/src/body.rs +++ b/src/body.rs @@ -9,8 +9,6 @@ use tower::BoxError; pub use hyper::body::Body; -use crate::BoxStdError; - /// A boxed [`Body`] trait object. pub struct BoxBody { // when we've gotten rid of `BoxStdError` we should be able to change the error type to @@ -78,3 +76,15 @@ where BoxBody::new(Full::from(s.into())) } } + +// work around for `BoxError` not implementing `std::error::Error` +// +// This is currently required since tower-http's Compression middleware's body type's +// error only implements error when the inner error type does: +// https://github.com/tower-rs/tower-http/blob/master/tower-http/src/lib.rs#L310 +// +// Fixing that is a breaking change to tower-http so we should wait a bit, but should +// totally fix it at some point. +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct BoxStdError(#[from] pub(crate) tower::BoxError); diff --git a/src/extract.rs b/src/extract/mod.rs similarity index 74% rename from src/extract.rs rename to src/extract/mod.rs index f8b4b3c6..b673d015 100644 --- a/src/extract.rs +++ b/src/extract/mod.rs @@ -1,10 +1,17 @@ use crate::{body::Body, response::IntoResponse}; use async_trait::async_trait; use bytes::Bytes; -use http::{header, Response, Request}; +use http::{header, Request, Response}; +use rejection::{ + BodyAlreadyTaken, FailedToBufferBody, InvalidJsonBody, InvalidUtf8, LengthRequired, + MissingExtension, MissingJsonContentType, MissingRouteParams, PayloadTooLarge, + QueryStringMissing, +}; use serde::de::DeserializeOwned; use std::{collections::HashMap, convert::Infallible, str::FromStr}; +pub mod rejection; + #[async_trait] pub trait FromRequest: Sized { type Rejection: IntoResponse; @@ -24,58 +31,6 @@ where } } -macro_rules! define_rejection { - ( - #[status = $status:ident] - #[body = $body:expr] - pub struct $name:ident (()); - ) => { - #[derive(Debug)] - pub struct $name(()); - - impl IntoResponse for $name { - fn into_response(self) -> http::Response { - let mut res = http::Response::new(Body::from($body)); - *res.status_mut() = http::StatusCode::$status; - res - } - } - }; - - ( - #[status = $status:ident] - #[body = $body:expr] - pub struct $name:ident (BoxError); - ) => { - #[derive(Debug)] - pub struct $name(tower::BoxError); - - impl $name { - fn from_err(err: E) -> Self - where - E: Into, - { - Self(err.into()) - } - } - - impl IntoResponse for $name { - fn into_response(self) -> http::Response { - let mut res = - http::Response::new(Body::from(format!(concat!($body, ": {}"), self.0))); - *res.status_mut() = http::StatusCode::$status; - res - } - } - }; -} - -define_rejection! { - #[status = BAD_REQUEST] - #[body = "Query string was invalid or missing"] - pub struct QueryStringMissing(()); -} - #[derive(Debug, Clone, Copy)] pub struct Query(pub T); @@ -96,18 +51,6 @@ where #[derive(Debug, Clone, Copy)] pub struct Json(pub T); -define_rejection! { - #[status = BAD_REQUEST] - #[body = "Failed to parse the response body as JSON"] - pub struct InvalidJsonBody(BoxError); -} - -define_rejection! { - #[status = BAD_REQUEST] - #[body = "Expected request with `Content-Type: application/json`"] - pub struct MissingJsonContentType(()); -} - #[async_trait] impl FromRequest for Json where @@ -116,7 +59,7 @@ where type Rejection = Response; async fn from_request(req: &mut Request) -> Result { - if has_content_type(&req, "application/json") { + if has_content_type(req, "application/json") { let body = take_body(req).map_err(IntoResponse::into_response)?; let bytes = hyper::body::to_bytes(body) @@ -151,12 +94,6 @@ fn has_content_type(req: &Request, expected_content_type: &str) -> bool { content_type.starts_with(expected_content_type) } -define_rejection! { - #[status = INTERNAL_SERVER_ERROR] - #[body = "Missing request extension"] - pub struct MissingExtension(()); -} - #[derive(Debug, Clone, Copy)] pub struct Extension(pub T); @@ -178,12 +115,6 @@ where } } -define_rejection! { - #[status = BAD_REQUEST] - #[body = "Failed to buffer the request body"] - pub struct FailedToBufferBody(BoxError); -} - #[async_trait] impl FromRequest for Bytes { type Rejection = Response; @@ -200,12 +131,6 @@ impl FromRequest for Bytes { } } -define_rejection! { - #[status = BAD_REQUEST] - #[body = "Response body didn't contain valid UTF-8"] - pub struct InvalidUtf8(BoxError); -} - #[async_trait] impl FromRequest for String { type Rejection = Response; @@ -236,18 +161,6 @@ impl FromRequest for Body { } } -define_rejection! { - #[status = PAYLOAD_TOO_LARGE] - #[body = "Request payload is too large"] - pub struct PayloadTooLarge(()); -} - -define_rejection! { - #[status = LENGTH_REQUIRED] - #[body = "Content length header is required"] - pub struct LengthRequired(()); -} - #[derive(Debug, Clone)] pub struct BytesMaxLength(pub Bytes); @@ -278,12 +191,6 @@ impl FromRequest for BytesMaxLength { } } -define_rejection! { - #[status = INTERNAL_SERVER_ERROR] - #[body = "No url params found for matched route. This is a bug in tower-web. Please open an issue"] - pub struct MissingRouteParams(()); -} - #[derive(Debug)] pub struct UrlParamsMap(HashMap); @@ -394,12 +301,6 @@ macro_rules! impl_parse_url { impl_parse_url!(T1, T2, T3, T4, T5, T6); -define_rejection! { - #[status = INTERNAL_SERVER_ERROR] - #[body = "Cannot have two request body extractors for a single handler"] - pub struct BodyAlreadyTaken(()); -} - fn take_body(req: &mut Request) -> Result { struct BodyAlreadyTakenExt; diff --git a/src/extract/rejection.rs b/src/extract/rejection.rs new file mode 100644 index 00000000..48284def --- /dev/null +++ b/src/extract/rejection.rs @@ -0,0 +1,108 @@ +use super::IntoResponse; +use crate::body::Body; + +macro_rules! define_rejection { + ( + #[status = $status:ident] + #[body = $body:expr] + pub struct $name:ident (()); + ) => { + #[derive(Debug)] + pub struct $name(pub(super) ()); + + impl IntoResponse for $name { + fn into_response(self) -> http::Response { + let mut res = http::Response::new(Body::from($body)); + *res.status_mut() = http::StatusCode::$status; + res + } + } + }; + + ( + #[status = $status:ident] + #[body = $body:expr] + pub struct $name:ident (BoxError); + ) => { + #[derive(Debug)] + pub struct $name(pub(super) tower::BoxError); + + impl $name { + pub(super) fn from_err(err: E) -> Self + where + E: Into, + { + Self(err.into()) + } + } + + impl IntoResponse for $name { + fn into_response(self) -> http::Response { + let mut res = + http::Response::new(Body::from(format!(concat!($body, ": {}"), self.0))); + *res.status_mut() = http::StatusCode::$status; + res + } + } + }; +} + +define_rejection! { + #[status = BAD_REQUEST] + #[body = "Query string was invalid or missing"] + pub struct QueryStringMissing(()); +} + +define_rejection! { + #[status = BAD_REQUEST] + #[body = "Failed to parse the response body as JSON"] + pub struct InvalidJsonBody(BoxError); +} + +define_rejection! { + #[status = BAD_REQUEST] + #[body = "Expected request with `Content-Type: application/json`"] + pub struct MissingJsonContentType(()); +} + +define_rejection! { + #[status = INTERNAL_SERVER_ERROR] + #[body = "Missing request extension"] + pub struct MissingExtension(()); +} + +define_rejection! { + #[status = BAD_REQUEST] + #[body = "Failed to buffer the request body"] + pub struct FailedToBufferBody(BoxError); +} + +define_rejection! { + #[status = BAD_REQUEST] + #[body = "Response body didn't contain valid UTF-8"] + pub struct InvalidUtf8(BoxError); +} + +define_rejection! { + #[status = PAYLOAD_TOO_LARGE] + #[body = "Request payload is too large"] + pub struct PayloadTooLarge(()); +} + +define_rejection! { + #[status = LENGTH_REQUIRED] + #[body = "Content length header is required"] + pub struct LengthRequired(()); +} + +define_rejection! { + #[status = INTERNAL_SERVER_ERROR] + #[body = "No url params found for matched route. This is a bug in tower-web. Please open an issue"] + pub struct MissingRouteParams(()); +} + +define_rejection! { + #[status = INTERNAL_SERVER_ERROR] + #[body = "Cannot have two request body extractors for a single handler"] + pub struct BodyAlreadyTaken(()); +} diff --git a/src/handler.rs b/src/handler.rs index f9f2aaae..4506a77b 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,4 +1,10 @@ -use crate::{body::Body, HandleError, extract::FromRequest, response::IntoResponse}; +use crate::{ + body::Body, + extract::FromRequest, + response::IntoResponse, + routing::{EmptyRouter, MethodFilter, OnMethod}, + service::{self, HandleError}, +}; use async_trait::async_trait; use bytes::Bytes; use futures_util::future; @@ -9,9 +15,32 @@ use std::{ marker::PhantomData, task::{Context, Poll}, }; -use tower::{Layer, BoxError, Service, ServiceExt}; +use tower::{BoxError, Layer, Service, ServiceExt}; + +pub fn get(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Get, handler) +} + +pub fn post(handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + on(MethodFilter::Post, handler) +} + +pub fn on(method: MethodFilter, handler: H) -> OnMethod, EmptyRouter> +where + H: Handler, +{ + service::on(method, handler.into_service()) +} mod sealed { + #![allow(unreachable_pub)] + pub trait HiddentTrait {} pub struct Hidden; impl HiddentTrait for Hidden {} @@ -30,9 +59,13 @@ pub trait Handler: Sized { fn layer(self, layer: L) -> Layered where - L: Layer>, + L: Layer>, { - Layered::new(layer.layer(HandlerSvc::new(self))) + Layered::new(layer.layer(IntoService::new(self))) + } + + fn into_service(self) -> IntoService { + IntoService::new(self) } } @@ -156,33 +189,33 @@ impl Layered { } } -pub struct HandlerSvc { +pub struct IntoService { handler: H, - _input: PhantomData (B, T)>, + _marker: PhantomData (B, T)>, } -impl HandlerSvc { - pub(crate) fn new(handler: H) -> Self { +impl IntoService { + fn new(handler: H) -> Self { Self { handler, - _input: PhantomData, + _marker: PhantomData, } } } -impl Clone for HandlerSvc +impl Clone for IntoService where H: Clone, { fn clone(&self) -> Self { Self { handler: self.handler.clone(), - _input: PhantomData, + _marker: PhantomData, } } } -impl Service> for HandlerSvc +impl Service> for IntoService where H: Handler + Clone + Send + 'static, H::Response: 'static, @@ -192,7 +225,7 @@ where type Future = future::BoxFuture<'static, Result>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - // HandlerSvc can only be constructed from async functions which are always ready, or from + // `IntoService` can only be constructed from async functions which are always ready, or from // `Layered` which bufferes in `::call` and is therefore also always // ready. Poll::Ready(Ok(())) diff --git a/src/lib.rs b/src/lib.rs index bd3a577b..b7e8043f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,70 +1,73 @@ +// #![doc(html_root_url = "https://docs.rs/tower-http/0.1.0")] +#![warn( + clippy::all, + clippy::dbg_macro, + clippy::todo, + clippy::empty_enum, + clippy::enum_glob_use, + clippy::pub_enum_variant_names, + clippy::mem_forget, + clippy::unused_self, + clippy::filter_map_next, + clippy::needless_continue, + clippy::needless_borrow, + clippy::match_wildcard_for_single_variants, + clippy::if_let_mutex, + clippy::mismatched_target_os, + clippy::await_holding_lock, + clippy::match_on_vec_items, + clippy::imprecise_flops, + clippy::suboptimal_flops, + clippy::lossy_float_literal, + clippy::rest_pat_in_fully_bound_structs, + clippy::fn_params_excessive_bools, + clippy::exit, + clippy::inefficient_to_string, + clippy::linkedlist, + clippy::macro_use_imports, + clippy::option_option, + clippy::verbose_file_reads, + clippy::unnested_or_patterns, + rust_2018_idioms, + future_incompatible, + nonstandard_style, + // missing_docs, +)] +#![deny(unreachable_pub, broken_intra_doc_links, private_in_public)] +#![allow( + elided_lifetimes_in_paths, + // TODO: Remove this once the MSRV bumps to 1.42.0 or above. + clippy::match_like_matches_macro, + clippy::type_complexity +)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(test, allow(clippy::float_cmp))] + use self::body::Body; -use body::BoxBody; use bytes::Bytes; -use futures_util::ready; -use handler::HandlerSvc; -use http::{Method, Request, Response}; -use pin_project::pin_project; +use http::{Request, Response}; use response::IntoResponse; -use routing::{EmptyRouter, OnMethod, Route}; -use std::{ - convert::Infallible, - fmt, - future::Future, - pin::Pin, - task::{Context, Poll}, -}; -use tower::{util::Oneshot, BoxError, Service, ServiceExt as _}; +use routing::{EmptyRouter, Route}; +use std::convert::Infallible; +use tower::{BoxError, Service}; pub mod body; pub mod extract; pub mod handler; pub mod response; pub mod routing; +pub mod service; #[doc(inline)] -pub use self::handler::Handler; -#[doc(inline)] -pub use self::routing::AddRoute; +pub use self::{ + handler::{get, on, post, Handler}, + routing::AddRoute, +}; pub use async_trait::async_trait; pub use tower_http::add_extension::{AddExtension, AddExtensionLayer}; -#[derive(Debug, Copy, Clone)] -pub enum MethodFilter { - Any, - Connect, - Delete, - Get, - Head, - Options, - Patch, - Post, - Put, - Trace, -} - -impl MethodFilter { - #[allow(clippy::match_like_matches_macro)] - fn matches(self, method: &Method) -> bool { - use MethodFilter::*; - - match (self, method) { - (Any, _) - | (Connect, &Method::CONNECT) - | (Delete, &Method::DELETE) - | (Get, &Method::GET) - | (Head, &Method::HEAD) - | (Options, &Method::OPTIONS) - | (Patch, &Method::PATCH) - | (Post, &Method::POST) - | (Put, &Method::PUT) - | (Trace, &Method::TRACE) => true, - _ => false, - } - } -} - pub fn route(spec: &str, svc: S) -> Route where S: Service, Error = Infallible> + Clone, @@ -72,28 +75,6 @@ where routing::EmptyRouter.route(spec, svc) } -pub fn get(handler: H) -> OnMethod, EmptyRouter> -where - H: Handler, -{ - on_method(MethodFilter::Get, HandlerSvc::new(handler)) -} - -pub fn post(handler: H) -> OnMethod, EmptyRouter> -where - H: Handler, -{ - on_method(MethodFilter::Post, HandlerSvc::new(handler)) -} - -pub fn on_method(method: MethodFilter, svc: S) -> OnMethod { - OnMethod { - method, - svc, - fallback: EmptyRouter, - } -} - #[cfg(test)] mod tests; @@ -110,20 +91,8 @@ impl ResultExt for Result { } } -// work around for `BoxError` not implementing `std::error::Error` -// -// This is currently required since tower-http's Compression middleware's body type's -// error only implements error when the inner error type does: -// https://github.com/tower-rs/tower-http/blob/master/tower-http/src/lib.rs#L310 -// -// Fixing that is a breaking change to tower-http so we should wait a bit, but should -// totally fix it at some point. -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct BoxStdError(#[from] pub(crate) tower::BoxError); - pub trait ServiceExt: Service, Response = Response> { - fn handle_error(self, f: F) -> HandleError + fn handle_error(self, f: F) -> service::HandleError where Self: Sized, F: FnOnce(Self::Error) -> Res, @@ -131,87 +100,8 @@ pub trait ServiceExt: Service, Response = Response> { B: http_body::Body + Send + Sync + 'static, B::Error: Into + Send + Sync + 'static, { - HandleError::new(self, f) + service::HandleError::new(self, f) } } impl ServiceExt for S where S: Service, Response = Response> {} - -#[derive(Clone)] -pub struct HandleError { - inner: S, - f: F, -} - -impl HandleError { - pub(crate) fn new(inner: S, f: F) -> Self { - Self { inner, f } - } -} - -impl fmt::Debug for HandleError -where - S: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("HandleError") - .field("inner", &self.inner) - .field("f", &format_args!("{}", std::any::type_name::())) - .finish() - } -} - -impl Service> for HandleError -where - S: Service, Response = Response> + Clone, - F: FnOnce(S::Error) -> Res + Clone, - Res: IntoResponse, - B: http_body::Body + Send + Sync + 'static, - B::Error: Into + Send + Sync + 'static, -{ - type Response = Response; - type Error = Infallible; - type Future = HandleErrorFuture>, F>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Request) -> Self::Future { - HandleErrorFuture { - f: Some(self.f.clone()), - inner: self.inner.clone().oneshot(req), - } - } -} - -#[pin_project] -pub struct HandleErrorFuture { - #[pin] - inner: Fut, - f: Option, -} - -impl Future for HandleErrorFuture -where - Fut: Future, E>>, - F: FnOnce(E) -> Res, - Res: IntoResponse, - B: http_body::Body + Send + Sync + 'static, - B::Error: Into + Send + Sync + 'static, -{ - type Output = Result, Infallible>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - - match ready!(this.inner.poll(cx)) { - Ok(res) => Ok(res.map(BoxBody::new)).into(), - Err(err) => { - let f = this.f.take().unwrap(); - let res = f(err).into_response(); - Ok(res.map(BoxBody::new)).into() - } - } - } -} diff --git a/src/response.rs b/src/response.rs index 8e722eee..ee0e8260 100644 --- a/src/response.rs +++ b/src/response.rs @@ -122,47 +122,6 @@ impl IntoResponse for std::borrow::Cow<'static, [u8]> { } } -pub struct Json(pub T); - -impl IntoResponse for Json -where - T: Serialize, -{ - fn into_response(self) -> Response { - let bytes = match serde_json::to_vec(&self.0) { - Ok(res) => res, - Err(err) => { - return Response::builder() - .header(header::CONTENT_TYPE, "text/plain") - .body(Body::from(err.to_string())) - .unwrap(); - } - }; - - let mut res = Response::new(Body::from(bytes)); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("application/json"), - ); - res - } -} - -pub struct Html(pub T); - -impl IntoResponse for Html -where - T: Into, -{ - fn into_response(self) -> Response { - let bytes = self.0.into(); - let mut res = Response::new(Body::from(bytes)); - res.headers_mut() - .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html")); - res - } -} - impl IntoResponse for StatusCode where B: Default, @@ -195,3 +154,57 @@ where res } } + +pub struct Html(pub T); + +impl IntoResponse for Html +where + T: Into, +{ + fn into_response(self) -> Response { + let mut res = Response::new(self.0.into()); + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html")); + res + } +} + +pub struct Json(pub T); + +impl IntoResponse for Json +where + T: Serialize, +{ + fn into_response(self) -> Response { + let bytes = match serde_json::to_vec(&self.0) { + Ok(res) => res, + Err(err) => { + return Response::builder() + .header(header::CONTENT_TYPE, "text/plain") + .body(Body::from(err.to_string())) + .unwrap(); + } + }; + + let mut res = Response::new(Body::from(bytes)); + res.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("application/json"), + ); + res + } +} + +pub struct Text(pub T); + +impl IntoResponse for Text +where + T: Into, +{ + fn into_response(self) -> Response { + let mut res = Response::new(self.0.into()); + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain")); + res + } +} diff --git a/src/routing.rs b/src/routing.rs index 7ecf1b03..d11d7142 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -1,12 +1,12 @@ use crate::{ body::BoxBody, - handler::{Handler, HandlerSvc}, + handler::{self, Handler}, response::IntoResponse, - MethodFilter, ResultExt, + ResultExt, }; use bytes::Bytes; use futures_util::{future, ready}; -use http::{Request, Response, StatusCode}; +use http::{Method, Request, Response, StatusCode}; use hyper::Body; use itertools::Itertools; use pin_project::pin_project; @@ -27,6 +27,39 @@ use tower::{ // ===== DSL ===== +#[derive(Debug, Copy, Clone)] +pub enum MethodFilter { + Any, + Connect, + Delete, + Get, + Head, + Options, + Patch, + Post, + Put, + Trace, +} + +impl MethodFilter { + #[allow(clippy::match_like_matches_macro)] + fn matches(self, method: &Method) -> bool { + match (self, method) { + (MethodFilter::Any, _) + | (MethodFilter::Connect, &Method::CONNECT) + | (MethodFilter::Delete, &Method::DELETE) + | (MethodFilter::Get, &Method::GET) + | (MethodFilter::Head, &Method::HEAD) + | (MethodFilter::Options, &Method::OPTIONS) + | (MethodFilter::Patch, &Method::PATCH) + | (MethodFilter::Post, &Method::POST) + | (MethodFilter::Put, &Method::PUT) + | (MethodFilter::Trace, &Method::TRACE) => true, + _ => false, + } + } +} + #[derive(Clone)] pub struct Route { pub(crate) pattern: PathPattern, @@ -84,21 +117,21 @@ impl AddRoute for Route { } impl OnMethod { - pub fn get(self, handler: H) -> OnMethod, Self> + pub fn get(self, handler: H) -> OnMethod, Self> where H: Handler, { - self.with_method(MethodFilter::Get, HandlerSvc::new(handler)) + self.on_method(MethodFilter::Get, handler.into_service()) } - pub fn post(self, handler: H) -> OnMethod, Self> + pub fn post(self, handler: H) -> OnMethod, Self> where H: Handler, { - self.with_method(MethodFilter::Post, HandlerSvc::new(handler)) + self.on_method(MethodFilter::Post, handler.into_service()) } - pub fn with_method(self, method: MethodFilter, svc: T) -> OnMethod { + pub fn on_method(self, method: MethodFilter, svc: T) -> OnMethod { OnMethod { method, svc, @@ -551,7 +584,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.matches(path).is_some(), "`{}` doesn't match `{}`", path, route_spec @@ -561,7 +594,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.matches(path).is_none(), "`{}` did match `{}` (but shouldn't)", path, route_spec diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 00000000..310c0faa --- /dev/null +++ b/src/service.rs @@ -0,0 +1,112 @@ +use crate::{ + body::{Body, BoxBody}, + response::IntoResponse, + routing::{EmptyRouter, MethodFilter, OnMethod}, +}; +use bytes::Bytes; +use futures_util::ready; +use http::{Request, Response}; +use pin_project::pin_project; +use std::{ + convert::Infallible, + fmt, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; +use tower::{util::Oneshot, BoxError, Service, ServiceExt as _}; + +pub fn get(svc: S) -> OnMethod { + on(MethodFilter::Get, svc) +} + +pub fn post(svc: S) -> OnMethod { + on(MethodFilter::Post, svc) +} + +pub fn on(method: MethodFilter, svc: S) -> OnMethod { + OnMethod { + method, + svc, + fallback: EmptyRouter, + } +} + +#[derive(Clone)] +pub struct HandleError { + inner: S, + f: F, +} + +impl HandleError { + pub(crate) fn new(inner: S, f: F) -> Self { + Self { inner, f } + } +} + +impl fmt::Debug for HandleError +where + S: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HandleError") + .field("inner", &self.inner) + .field("f", &format_args!("{}", std::any::type_name::())) + .finish() + } +} + +impl Service> for HandleError +where + S: Service, Response = Response> + Clone, + F: FnOnce(S::Error) -> Res + Clone, + Res: IntoResponse, + B: http_body::Body + Send + Sync + 'static, + B::Error: Into + Send + Sync + 'static, +{ + type Response = Response; + type Error = Infallible; + type Future = HandleErrorFuture>, F>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + HandleErrorFuture { + f: Some(self.f.clone()), + inner: self.inner.clone().oneshot(req), + } + } +} + +#[pin_project] +pub struct HandleErrorFuture { + #[pin] + inner: Fut, + f: Option, +} + +impl Future for HandleErrorFuture +where + Fut: Future, E>>, + F: FnOnce(E) -> Res, + Res: IntoResponse, + B: http_body::Body + Send + Sync + 'static, + B::Error: Into + Send + Sync + 'static, +{ + type Output = Result, Infallible>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + + match ready!(this.inner.poll(cx)) { + Ok(res) => Ok(res.map(BoxBody::new)).into(), + Err(err) => { + let f = this.f.take().unwrap(); + let res = f(err).into_response(); + Ok(res.map(BoxBody::new)).into() + } + } + } +} diff --git a/src/tests.rs b/src/tests.rs index 039503ce..ca96b30c 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,4 +1,4 @@ -use crate::{extract, get, on_method, post, route, AddRoute, Handler, MethodFilter}; +use crate::{extract, get, post, route, routing::MethodFilter, service, AddRoute, Handler}; use http::{Request, Response, StatusCode}; use hyper::{Body, Server}; use serde::Deserialize; @@ -307,7 +307,7 @@ async fn service_handlers() { let app = route( "/echo", - on_method( + service::on( MethodFilter::Post, service_fn(|req: Request| async move { Ok::<_, Infallible>(Response::new(req.into_body())) @@ -316,7 +316,7 @@ async fn service_handlers() { ) .route( "/static/Cargo.toml", - on_method( + service::on( MethodFilter::Get, ServeFile::new("Cargo.toml").handle_error(|error: std::io::Error| { (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()) @@ -564,7 +564,7 @@ async fn layer_on_whole_router() { // // TODO(david): composing two apps that have had layers applied /// Run a `tower::Service` in the background and get a URI for it. -pub async fn run_in_background(svc: S) -> SocketAddr +async fn run_in_background(svc: S) -> SocketAddr where S: Service, Response = Response> + Clone + Send + 'static, ResBody: http_body::Body + Send + 'static,