diff --git a/README.md b/README.md index 171c1a4f..048f1216 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,15 @@ handle(Request) -> Response`. - Solid foundation. tower-web is built on top of tower and makes it easy to plug in any middleware from the [tower] and [tower-http] ecosystem. - Focus on routing, extracting data from requests, and generating responses. -tower middleware can handle the rest. +Tower middleware can handle the rest. - Macro free core. Macro frameworks have their place but tower-web focuses on providing a core that is macro free. +## Compatibility + +tower-web is designed to work with [tokio] and [hyper]. Runtime and +transport layer independence is not a goal, at least for the time being. + ## Example The "Hello, World!" of tower-web is: @@ -33,8 +38,10 @@ async fn main() { // run it with hyper on localhost:3000 let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - let server = Server::bind(&addr).serve(Shared::new(app)); - server.await.unwrap(); + Server::bind(&addr) + .serve(Shared::new(app)) + .await + .unwrap(); } ``` @@ -66,7 +73,8 @@ requests"](#extracting-data-from-requests) for more details on that. ## Responses -Anything that implements [`IntoResponse`] can be returned from a handler: +Anything that implements [`IntoResponse`](response::IntoResponse) can be +returned from a handler: ```rust use tower_web::{body::Body, response::{Html, Json}, prelude::*}; @@ -107,7 +115,7 @@ async fn html(req: Request) -> Html<&'static str> { Html("

Hello, World!

") } -// `Json` gives a content-type of `application/json` and works with my type +// `Json` gives a content-type of `application/json` and works with any type // that implements `serde::Serialize` async fn json(req: Request) -> Json { Json(json!({ "data": 42 })) @@ -145,8 +153,8 @@ but any arguments following are called "extractors". Any type that implements [`FromRequest`](crate::extract::FromRequest) can be used as an extractor. -[`extract::Json`] is an extractor that consumes the request body and -deserializes as as JSON into some target type: +For example, [`extract::Json`] is an extractor that consumes the request body and +deserializes it as JSON into some target type: ```rust use tower_web::prelude::*; @@ -178,7 +186,7 @@ use uuid::Uuid; let app = route("/users/:id", post(create_user)); async fn create_user(req: Request, params: extract::UrlParams<(Uuid,)>) { - let (user_id,) = params.0; + let user_id: Uuid = (params.0).0; // ... } @@ -229,7 +237,7 @@ See the [`extract`] module for more details. tower-web is designed to take full advantage of the tower and tower-http ecosystem of middleware: -### To individual handlers +### Applying middleware to individual handlers A middleware can be applied to a single handler like so: @@ -245,7 +253,7 @@ let app = route( async fn handler(req: Request) {} ``` -### To groups of routes +### Applying middleware to groups of routes Middleware can also be applied to a group of routes like so: @@ -269,11 +277,12 @@ tower-web requires all errors to be handled. That is done by using implementations. For handlers created from async functions this is works automatically since -handlers must return something that implements [`IntoResponse`], even if its -a `Result`. +handlers must return something that implements +[`IntoResponse`](response::IntoResponse), even if its a `Result`. However middleware might add new failure cases that has to be handled. For -that tower-web provides a `handle_error` combinator: +that tower-web provides a [`handle_error`](handler::Layered::handle_error) +combinator: ```rust use tower_web::prelude::*; @@ -308,27 +317,27 @@ let app = route( async fn handle(req: Request) {} ``` -The closure passed to `handle_error` must return something that implements -`IntoResponse`. +The closure passed to [`handle_error`](handler::Layered::handle_error) must +return something that implements [`IntoResponse`](response::IntoResponse). -`handle_error` is also available on a group of routes with middleware -applied: +[`handle_error`](routing::Layered::handle_error) is also available on a +group of routes with middleware applied: ```rust use tower_web::prelude::*; -use tower::{ - BoxError, timeout::{TimeoutLayer, error::Elapsed}, -}; -use std::{borrow::Cow, time::Duration}; -use http::StatusCode; +use tower::{BoxError, timeout::TimeoutLayer}; +use std::time::Duration; let app = route("/", get(handle)) + .route("/foo", post(other_handle)) .layer(TimeoutLayer::new(Duration::from_secs(30))) .handle_error(|error: BoxError| { // ... }); async fn handle(req: Request) {} + +async fn other_handle(req: Request) {} ``` ### Applying multiple middleware @@ -416,9 +425,8 @@ tower-web also supports routing to general [`Service`]s: ```rust use tower_web::{ - service, prelude::*, // `ServiceExt` adds `handle_error` to any `Service` - ServiceExt, + service::{self, ServiceExt}, prelude::*, }; use tower_http::services::ServeFile; use http::Response; @@ -447,7 +455,7 @@ See the [`service`] module for more details. ## Nesting applications -Applications can be nested by calling `nest`: +Applications can be nested by calling [`nest`](routing::nest): ```rust use tower_web::{prelude::*, routing::BoxRoute, body::BoxBody}; @@ -463,10 +471,10 @@ let app = route("/", get(|_: Request| async { /* ... */ })) .nest("/api", api_routes()); ``` -`nest` can also be used to serve static files from a directory: +[`nest`](routing::nest) can also be used to serve static files from a directory: ```rust -use tower_web::{prelude::*, ServiceExt, routing::nest}; +use tower_web::{prelude::*, service::ServiceExt, routing::nest}; use tower_http::services::ServeDir; use http::Response; use std::convert::Infallible; @@ -482,3 +490,5 @@ let app = nest( [tower]: https://crates.io/crates/tower [tower-http]: https://crates.io/crates/tower-http +[tokio]: http://crates.io/crates/tokio +[hyper]: http://crates.io/crates/hyper diff --git a/src/body.rs b/src/body.rs index b490dd97..6ba65cbd 100644 --- a/src/body.rs +++ b/src/body.rs @@ -12,6 +12,9 @@ use tower::BoxError; pub use hyper::body::Body; /// A boxed [`Body`] trait object. +/// +/// This is used in tower-web as the response body type for applications. Its necessary to unify +/// multiple response bodies types into one. pub struct BoxBody { // when we've gotten rid of `BoxStdError` we should be able to change the error type to // `BoxError` diff --git a/src/extract/mod.rs b/src/extract/mod.rs index b90f6058..5a870f68 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -1,4 +1,142 @@ //! Types and traits for extracting data from requests. +//! +//! A handler function must always take `Request` as its first argument +//! but any arguments following are called "extractors". Any type that +//! implements [`FromRequest`](FromRequest) can be used as an extractor. +//! +//! For example, [`Json`] is an extractor that consumes the request body and +//! deserializes it as JSON into some target type: +//! +//! ```rust,no_run +//! use tower_web::prelude::*; +//! use serde::Deserialize; +//! +//! #[derive(Deserialize)] +//! struct CreateUser { +//! email: String, +//! password: String, +//! } +//! +//! async fn create_user(req: Request, payload: extract::Json) { +//! let payload: CreateUser = payload.0; +//! +//! // ... +//! } +//! +//! let app = route("/users", post(create_user)); +//! # async { +//! # hyper::Server::bind(&"".parse().unwrap()).serve(tower::make::Shared::new(app)).await; +//! # }; +//! ``` +//! +//! Technically extractors can also be used as "guards", for example to require +//! that requests are authorized. However the recommended way to do that is +//! using Tower middleware, such as [`tower_http::auth::RequireAuthorization`]. +//! Extractors have to be applied to each handler, whereas middleware can be +//! applied to a whole stack at once, which is typically what you want for +//! authorization. +//! +//! # Defining custom extractors +//! +//! You can also define your own extractors by implementing [`FromRequest`]: +//! +//! ```rust,no_run +//! use tower_web::{async_trait, extract::FromRequest, prelude::*}; +//! use http::{StatusCode, header::{HeaderValue, USER_AGENT}}; +//! +//! struct ExtractUserAgent(HeaderValue); +//! +//! #[async_trait] +//! impl FromRequest for ExtractUserAgent { +//! type Rejection = (StatusCode, &'static str); +//! +//! async fn from_request(req: &mut Request) -> Result { +//! if let Some(user_agent) = req.headers().get(USER_AGENT) { +//! Ok(ExtractUserAgent(user_agent.clone())) +//! } else { +//! Err((StatusCode::BAD_REQUEST, "`User-Agent` header is missing")) +//! } +//! } +//! } +//! +//! async fn handler(req: Request, user_agent: ExtractUserAgent) { +//! let user_agent: HeaderValue = user_agent.0; +//! +//! // ... +//! } +//! +//! let app = route("/foo", get(handler)); +//! # async { +//! # hyper::Server::bind(&"".parse().unwrap()).serve(tower::make::Shared::new(app)).await; +//! # }; +//! ``` +//! +//! # Multiple extractors +//! +//! Handlers can also contain multiple extractors: +//! +//! ```rust,no_run +//! use tower_web::prelude::*; +//! use std::collections::HashMap; +//! +//! async fn handler( +//! req: Request, +//! // Extract captured parameters from the URL +//! params: extract::UrlParamsMap, +//! // Parse query string into a `HashMap` +//! query_params: extract::Query>, +//! // Buffer the request body into a `Bytes` +//! bytes: bytes::Bytes, +//! ) { +//! // ... +//! } +//! +//! let app = route("/foo", get(handler)); +//! # async { +//! # hyper::Server::bind(&"".parse().unwrap()).serve(tower::make::Shared::new(app)).await; +//! # }; +//! ``` +//! +//! # Optional extractors +//! +//! Wrapping extractors in `Option` will make them optional: +//! +//! ```rust,no_run +//! use tower_web::{extract::Json, prelude::*}; +//! use serde_json::Value; +//! +//! async fn create_user(req: Request, payload: Option>) { +//! if let Some(payload) = payload { +//! // We got a valid JSON payload +//! } else { +//! // Payload wasn't valid JSON +//! } +//! } +//! +//! let app = route("/users", post(create_user)); +//! # async { +//! # hyper::Server::bind(&"".parse().unwrap()).serve(tower::make::Shared::new(app)).await; +//! # }; +//! ``` +//! +//! # Reducing boilerplate +//! +//! If you're feeling adventorous you can even deconstruct the extractors +//! directly on the function signature: +//! +//! ```rust,no_run +//! use tower_web::{extract::Json, prelude::*}; +//! use serde_json::Value; +//! +//! async fn create_user(req: Request, Json(value): Json) { +//! // `value` is of type `Value` +//! } +//! +//! let app = route("/users", post(create_user)); +//! # async { +//! # hyper::Server::bind(&"".parse().unwrap()).serve(tower::make::Shared::new(app)).await; +//! # }; +//! ``` use crate::{body::Body, response::IntoResponse}; use async_trait::async_trait; @@ -7,17 +145,23 @@ use http::{header, Request, Response}; use rejection::{ BodyAlreadyTaken, FailedToBufferBody, InvalidJsonBody, InvalidUrlParam, InvalidUtf8, LengthRequired, MissingExtension, MissingJsonContentType, MissingRouteParams, PayloadTooLarge, - QueryStringMissing, + QueryStringMissing, UrlParamsAlreadyTaken, }; use serde::de::DeserializeOwned; use std::{collections::HashMap, convert::Infallible, str::FromStr}; pub mod rejection; +/// Types that can be created from requests. +/// +/// See the [module docs](crate::extract) for more details. #[async_trait] pub trait FromRequest: Sized { + /// If the extractor fails it'll use this "rejection" type. A rejection is + /// a kind of error that can be converted into a response. type Rejection: IntoResponse; + /// Perform the extraction. async fn from_request(req: &mut Request) -> Result; } @@ -33,6 +177,34 @@ where } } +/// Extractor that deserializes query strings into some type. +/// +/// `T` is expected to implement [`serde::Deserialize`]. +/// +/// # Example +/// +/// ```rust,no_run +/// use tower_web::prelude::*; +/// use serde::Deserialize; +/// +/// #[derive(Deserialize)] +/// struct Pagination { +/// page: usize, +/// per_page: usize, +/// } +/// +/// // This will parse query strings like `?page=2&per_page=30` into `Pagination` +/// // structs. +/// async fn list_things(req: Request, pagination: extract::Query) { +/// let pagination: Pagination = pagination.0; +/// +/// // ... +/// } +/// let app = route("/list_things", get(list_things)); +/// ``` +/// +/// If the query string cannot be parsed it will reject the request with a `404 +/// Bad Request` response. #[derive(Debug, Clone, Copy, Default)] pub struct Query(pub T); @@ -50,6 +222,35 @@ where } } +/// Extractor that deserializes request bodies into some type. +/// +/// `T` is expected to implement [`serde::Deserialize`]. +/// +/// # Example +/// +/// ```rust,no_run +/// use tower_web::prelude::*; +/// use serde::Deserialize; +/// +/// #[derive(Deserialize)] +/// struct CreateUser { +/// email: String, +/// password: String, +/// } +/// +/// async fn create_user(req: Request, payload: extract::Json) { +/// let payload: CreateUser = payload.0; +/// +/// // ... +/// } +/// +/// let app = route("/users", post(create_user)); +/// ``` +/// +/// If the query string cannot be parsed it will reject the request with a `404 +/// Bad Request` response. +/// +/// The request is required to have a `Content-Type: application/json` header. #[derive(Debug, Clone, Copy, Default)] pub struct Json(pub T); @@ -61,15 +262,17 @@ where type Rejection = Response; async fn from_request(req: &mut Request) -> Result { + use bytes::Buf; + if has_content_type(req, "application/json") { let body = take_body(req).map_err(IntoResponse::into_response)?; - let bytes = hyper::body::to_bytes(body) + let buf = hyper::body::aggregate(body) .await .map_err(InvalidJsonBody::from_err) .map_err(IntoResponse::into_response)?; - let value = serde_json::from_slice(&bytes) + let value = serde_json::from_reader(buf.reader()) .map_err(InvalidJsonBody::from_err) .map_err(IntoResponse::into_response)?; @@ -96,6 +299,35 @@ fn has_content_type(req: &Request, expected_content_type: &str) -> bool { content_type.starts_with(expected_content_type) } +/// Extractor that gets a value from request extensions. +/// +/// This is commonly used to share state across handlers. +/// +/// # Example +/// +/// ```rust,no_run +/// use tower_web::{AddExtensionLayer, prelude::*}; +/// use std::sync::Arc; +/// +/// // Some shared state used throughout our application +/// struct State { +/// // ... +/// } +/// +/// async fn handler(req: Request, state: extract::Extension>) { +/// // ... +/// } +/// +/// let state = Arc::new(State { /* ... */ }); +/// +/// let app = route("/", get(handler)) +/// // Add middleware that inserts the state into all incoming request's +/// // extensions. +/// .layer(AddExtensionLayer::new(state)); +/// ``` +/// +/// If the extension is missing it will reject the request with a `500 Interal +/// Server Error` response. #[derive(Debug, Clone, Copy)] pub struct Extension(pub T); @@ -163,6 +395,21 @@ impl FromRequest for Body { } } +/// Extractor that will buffer request bodies up to a certain size. +/// +/// # Example +/// +/// ```rust,no_run +/// use tower_web::prelude::*; +/// +/// async fn handler(req: Request, body: extract::BytesMaxLength<1024>) { +/// // ... +/// } +/// +/// let app = route("/", post(handler)); +/// ``` +/// +/// This requires the request to have a `Content-Length` header. #[derive(Debug, Clone)] pub struct BytesMaxLength(pub Bytes); @@ -193,39 +440,86 @@ impl FromRequest for BytesMaxLength { } } +/// Extractor that will get captures from the URL. +/// +/// # Example +/// +/// ```rust,no_run +/// use tower_web::prelude::*; +/// +/// async fn users_show(req: Request, params: extract::UrlParamsMap) { +/// let id: Option<&str> = params.get("id"); +/// +/// // ... +/// } +/// +/// let app = route("/users/:id", get(users_show)); +/// ``` +/// +/// Note that you can only have one URL params extractor per handler. If you +/// have multiple it'll response with `500 Internal Server Error`. #[derive(Debug)] pub struct UrlParamsMap(HashMap); impl UrlParamsMap { + /// Look up the value for a key. pub fn get(&self, key: &str) -> Option<&str> { self.0.get(key).map(|s| &**s) } - pub fn get_typed(&self, key: &str) -> Option + /// Look up the value for a key and parse it into a value of type `T`. + pub fn get_typed(&self, key: &str) -> Option> where T: FromStr, { - self.get(key)?.parse().ok() + self.get(key).map(str::parse) } } #[async_trait] impl FromRequest for UrlParamsMap { - type Rejection = MissingRouteParams; + type Rejection = Response; async fn from_request(req: &mut Request) -> Result { if let Some(params) = req .extensions_mut() .get_mut::>() { - let params = params.take().expect("params already taken").0; - Ok(Self(params.into_iter().collect())) + if let Some(params) = params.take() { + Ok(Self(params.0.into_iter().collect())) + } else { + Err(UrlParamsAlreadyTaken.into_response()) + } } else { - Err(MissingRouteParams) + Err(MissingRouteParams.into_response()) } } } +/// Extractor that will get captures from the URL and parse them. +/// +/// # Example +/// +/// ```rust,no_run +/// use tower_web::{extract::UrlParams, prelude::*}; +/// use uuid::Uuid; +/// +/// async fn users_teams_show( +/// req: Request, +/// UrlParams(params): UrlParams<(Uuid, Uuid)>, +/// ) { +/// let user_id: Uuid = params.0; +/// let team_id: Uuid = params.1; +/// +/// // ... +/// } +/// +/// let app = route("/users/:user_id/team/:team_id", get(users_teams_show)); +/// ``` +/// +/// Note that you can only have one URL params extractor per handler. If you +/// have multiple it'll response with `500 Internal Server Error`. +#[derive(Debug)] pub struct UrlParams(pub T); macro_rules! impl_parse_url { @@ -246,7 +540,11 @@ macro_rules! impl_parse_url { .extensions_mut() .get_mut::>() { - params.take().expect("params already taken").0 + if let Some(params) = params.take() { + params.0 + } else { + return Err(UrlParamsAlreadyTaken.into_response()); + } } else { return Err(MissingRouteParams.into_response()) }; diff --git a/src/extract/rejection.rs b/src/extract/rejection.rs index 08aeaac9..f1b13ca3 100644 --- a/src/extract/rejection.rs +++ b/src/extract/rejection.rs @@ -125,6 +125,13 @@ define_rejection! { pub struct MissingRouteParams; } +define_rejection! { + #[status = INTERNAL_SERVER_ERROR] + #[body = "Cannot have two URL capture extractors for a single handler"] + /// Rejection type used if you try and extract the URL params more than once. + pub struct UrlParamsAlreadyTaken; +} + define_rejection! { #[status = INTERNAL_SERVER_ERROR] #[body = "Cannot have two request body extractors for a single handler"] diff --git a/src/lib.rs b/src/lib.rs index bab2aeba..1fb4759d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,11 @@ //! - Macro free core. Macro frameworks have their place but tower-web focuses //! on providing a core that is macro free. //! +//! # Compatibility +//! +//! tower-web is designed to work with [tokio] and [hyper]. Runtime and +//! transport layer independence is not a goal, at least for the time being. +//! //! # Example //! //! The "Hello, World!" of tower-web is: @@ -31,8 +36,10 @@ //! //! // run it with hyper on localhost:3000 //! let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); -//! let server = Server::bind(&addr).serve(Shared::new(app)); -//! server.await.unwrap(); +//! Server::bind(&addr) +//! .serve(Shared::new(app)) +//! .await +//! .unwrap(); //! } //! ``` //! @@ -109,7 +116,7 @@ //! Html("

Hello, World!

") //! } //! -//! // `Json` gives a content-type of `application/json` and works with my type +//! // `Json` gives a content-type of `application/json` and works with any type //! // that implements `serde::Serialize` //! async fn json(req: Request) -> Json { //! Json(json!({ "data": 42 })) @@ -150,8 +157,8 @@ //! implements [`FromRequest`](crate::extract::FromRequest) can be used as an //! extractor. //! -//! [`extract::Json`] is an extractor that consumes the request body and -//! deserializes as as JSON into some target type: +//! For example, [`extract::Json`] is an extractor that consumes the request body and +//! deserializes it as JSON into some target type: //! //! ```rust,no_run //! use tower_web::prelude::*; @@ -186,7 +193,7 @@ //! let app = route("/users/:id", post(create_user)); //! //! async fn create_user(req: Request, params: extract::UrlParams<(Uuid,)>) { -//! let (user_id,) = params.0; +//! let user_id: Uuid = (params.0).0; //! //! // ... //! } @@ -243,7 +250,7 @@ //! tower-web is designed to take full advantage of the tower and tower-http //! ecosystem of middleware: //! -//! ## To individual handlers +//! ## Applying middleware to individual handlers //! //! A middleware can be applied to a single handler like so: //! @@ -262,7 +269,7 @@ //! # }; //! ``` //! -//! ## To groups of routes +//! ## Applying middleware to groups of routes //! //! Middleware can also be applied to a group of routes like so: //! @@ -293,7 +300,8 @@ //! [`IntoResponse`](response::IntoResponse), even if its a `Result`. //! //! However middleware might add new failure cases that has to be handled. For -//! that tower-web provides a `handle_error` combinator: +//! that tower-web provides a [`handle_error`](handler::Layered::handle_error) +//! combinator: //! //! ```rust,no_run //! use tower_web::prelude::*; @@ -331,27 +339,27 @@ //! # }; //! ``` //! -//! The closure passed to `handle_error` must return something that implements -//! `IntoResponse`. +//! The closure passed to [`handle_error`](handler::Layered::handle_error) must +//! return something that implements [`IntoResponse`](response::IntoResponse). //! -//! `handle_error` is also available on a group of routes with middleware -//! applied: +//! [`handle_error`](routing::Layered::handle_error) is also available on a +//! group of routes with middleware applied: //! //! ```rust,no_run //! use tower_web::prelude::*; -//! use tower::{ -//! BoxError, timeout::{TimeoutLayer, error::Elapsed}, -//! }; -//! use std::{borrow::Cow, time::Duration}; -//! use http::StatusCode; +//! use tower::{BoxError, timeout::TimeoutLayer}; +//! use std::time::Duration; //! //! let app = route("/", get(handle)) +//! .route("/foo", post(other_handle)) //! .layer(TimeoutLayer::new(Duration::from_secs(30))) //! .handle_error(|error: BoxError| { //! // ... //! }); //! //! async fn handle(req: Request) {} +//! +//! async fn other_handle(req: Request) {} //! # async { //! # hyper::Server::bind(&"".parse().unwrap()).serve(tower::make::Shared::new(app)).await; //! # }; @@ -481,7 +489,7 @@ //! //! # Nesting applications //! -//! Applications can be nested by calling `nest`: +//! Applications can be nested by calling [`nest`](routing::nest): //! //! ```rust,no_run //! use tower_web::{prelude::*, routing::BoxRoute, body::BoxBody}; @@ -500,7 +508,7 @@ //! # }; //! ``` //! -//! `nest` can also be used to serve static files from a directory: +//! [`nest`](routing::nest) can also be used to serve static files from a directory: //! //! ```rust,no_run //! use tower_web::{prelude::*, service::ServiceExt, routing::nest}; @@ -522,6 +530,8 @@ //! //! [tower]: https://crates.io/crates/tower //! [tower-http]: https://crates.io/crates/tower-http +//! [tokio]: http://crates.io/crates/tokio +//! [hyper]: http://crates.io/crates/hyper // #![doc(html_root_url = "https://docs.rs/tower-http/0.1.0")] #![warn( diff --git a/src/tests.rs b/src/tests.rs index 2380fcfb..49c13beb 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -248,7 +248,13 @@ async fn extracting_url_params() { .post( |_: Request, params_map: extract::UrlParamsMap| async move { assert_eq!(params_map.get("id").unwrap(), "1337"); - assert_eq!(params_map.get_typed::("id").unwrap(), 1337); + assert_eq!( + params_map + .get_typed::("id") + .expect("missing") + .expect("failed to parse"), + 1337 + ); }, ), );