From a5e0c03f21b86a3b25c3f72149bf0881c48081a1 Mon Sep 17 00:00:00 2001 From: Aaron Turon Date: Thu, 21 Mar 2019 07:51:47 -0700 Subject: [PATCH] Revamp Tide, dropping Extractors and simplifying the framework This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint: Send + Sync + 'static { /// The async result of `call`. type Fut: Future + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl Endpoint for F where F: Fn(Context) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl Context { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl Context { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl Context { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl Context { pub async fn body_bytes(&mut self) -> std::io::Result>; pub async fn body_string(&mut self) -> std::io::Result; pub async fn body_json(&mut self) -> std::io::Result; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData, msg: body::Json) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData, id: head::Path, msg: body::Json, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context) -> Result { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl Cookies for Context { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl Context { pub async fn body_json_cfg(&mut self, cfg: JsonConfig) -> std::io::Result; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue #12 and issue #141 for more details. --- Cargo.toml | 5 +- src/app.rs | 530 ++++++++++++++++------ src/body.rs | 351 --------------- src/configuration/default_config.rs | 79 ---- src/configuration/mod.rs | 156 ------- src/context.rs | 87 ++++ src/cookies.rs | 48 +- src/endpoint.rs | 125 ++---- src/extract.rs | 20 - src/forms.rs | 48 ++ src/head.rs | 225 ---------- src/lib.rs | 39 +- src/middleware/default_headers.rs | 29 +- src/middleware/logger.rs | 31 +- src/middleware/mod.rs | 64 +-- src/request.rs | 65 --- src/response.rs | 22 +- src/route.rs | 80 ++++ src/router.rs | 654 ++-------------------------- src/serve.rs | 4 - 20 files changed, 771 insertions(+), 1891 deletions(-) delete mode 100644 src/body.rs delete mode 100644 src/configuration/default_config.rs delete mode 100644 src/configuration/mod.rs create mode 100644 src/context.rs delete mode 100644 src/extract.rs create mode 100644 src/forms.rs delete mode 100644 src/head.rs delete mode 100644 src/request.rs create mode 100644 src/route.rs delete mode 100644 src/serve.rs diff --git a/Cargo.toml b/Cargo.toml index a32df42ad..e05bc2e18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,11 @@ version = "0.0.5" [dependencies] cookie = "0.11" futures-preview = "0.3.0-alpha.13" +fnv = "1.0.6" http = "0.1" http-service = "0.1.4" -path-table = "1.0.0" pin-utils = "0.1.0-alpha.4" +route-recognizer = "0.1.12" serde = "1.0.80" serde_derive = "1.0.80" serde_json = "1.0.32" @@ -45,4 +46,4 @@ hyper = ["http-service-hyper"] basic-cookies = "0.1.3" juniper = "0.10.0" structopt = "0.2.14" -http-service-mock = "0.1.0" \ No newline at end of file +http-service-mock = "0.1.0" diff --git a/src/app.rs b/src/app.rs index e86419539..028b1120e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,20 +1,11 @@ use futures::future::{self, FutureObj}; use http_service::HttpService; -use std::{ - any::Any, - fmt::Debug, - ops::{Deref, DerefMut}, - sync::Arc, -}; +use std::sync::Arc; use crate::{ - configuration::{Configuration, Store}, - endpoint::BoxedEndpoint, - endpoint::Endpoint, - extract::Extract, - middleware::{logger::RootLogger, RequestContext}, - router::{EndpointData, Resource, RouteResult, Router}, - Middleware, Request, Response, RouteMatch, + middleware::{Middleware, Next}, + router::{RouteResult, Router}, + Context, Route, }; /// The top-level type for setting up a Tide application. @@ -71,86 +62,78 @@ use crate::{ /// Where to go from here: Please see [`Router`](struct.Router.html) and [`Endpoint`](trait.Endpoint.html) /// for further examples. /// -pub struct App { - data: Data, - router: Router, - default_handler: EndpointData, +/// A core type for routing. +/// +/// The `App` type can be used to set up routes and resources, and to apply middleware. +pub struct App { + router: Router, + middleware: Vec>>, + data: AppData, } -impl App { - /// Set up a new app with some initial `data`. - pub fn new(data: Data) -> App { - let logger = RootLogger::new(); - let mut app = App { - data, +impl App { + /// Create an empty `App`, with no initial middleware or configuration. + pub fn new(data: AppData) -> App { + App { router: Router::new(), - default_handler: EndpointData { - endpoint: BoxedEndpoint::new(async || http::status::StatusCode::NOT_FOUND), - store: Store::new(), - }, - }; - - // Add RootLogger as a default middleware - app.middleware(logger); - app.setup_configuration(); - - app - } - - // Add default configuration - fn setup_configuration(&mut self) { - let config = Configuration::build().finalize(); - self.config(config); - } - - /// Get the top-level router. - pub fn router(&mut self) -> &mut Router { - &mut self.router - } - - /// Add a new resource at `path`. - /// See [Router.at](struct.Router.html#method.at) for details. - pub fn at<'a>(&'a mut self, path: &'a str) -> Resource<'a, Data> { - self.router.at(path) - } - - /// Set the default handler for the app, a fallback function when there is no match to the route requested - pub fn default_handler, U>( - &mut self, - handler: T, - ) -> &mut EndpointData { - let endpoint = EndpointData { - endpoint: BoxedEndpoint::new(handler), - store: self.router.store_base.clone(), - }; - self.default_handler = endpoint; - &mut self.default_handler - } - - /// Apply `middleware` to the whole app. Note that the order of nesting subrouters and applying - /// middleware matters; see `Router` for details. - pub fn middleware(&mut self, middleware: impl Middleware + 'static) -> &mut Self { - self.router.middleware(middleware); - self - } - - /// Add a default configuration `item` for the whole app. - pub fn config(&mut self, item: T) -> &mut Self { - self.router.config(item); - self + middleware: Vec::new(), + data, + } } - pub fn get_item(&self) -> Option<&T> { - self.router.get_item() + /// Add a new resource at the given `path`, relative to this router. + /// + /// Routing means mapping an HTTP request to an endpoint. Here Tide applies a "table of + /// contents" approach, which makes it easy to see the overall app structure. Endpoints are + /// selected solely by the path and HTTP method of a request: the path determines the resource + /// and the HTTP verb the respective endpoint of the selected resource. Example: + /// + /// ```rust,no_run + /// # #![feature(async_await)] + /// # let mut app = tide::App::new(()); + /// app.at("/").get(async || "Hello, world!"); + /// ``` + /// + /// A path is comprised of zero or many segments, i.e. non-empty strings separated by '/'. There + /// are two kinds of segments: concrete and wildcard. A concrete segment is used to exactly + /// match the respective part of the path of the incoming request. A wildcard segment on the + /// other hand extracts and parses the respective part of the path of the incoming request to + /// pass it along to the endpoint as an argument. A wildcard segment is either defined by "{}" + /// or by "{name}" for a so called named wildcard segment which can be extracted using + /// `NamedSegment`. It is not possible to define wildcard segments with different names for + /// otherwise identical paths. + /// + /// Wildcard definitions can be followed by an optional *wildcard modifier*. Currently, there is + /// only one modifier: `*`, which means that the wildcard will match to the end of given path, + /// no matter how many segments are left, even nothing. If there is a modifier for unnamed + /// wildcard definition, `{}` may be omitted. That is, `{}*` can be written as `*`. It is an + /// error to define two wildcard segments with different wildcard modifiers, or to write other + /// path segment after a segment with wildcard modifier. + /// + /// Here are some examples omitting the HTTP verb based endpoint selection: + /// + /// ```rust,no_run + /// # let mut app = tide::App::new(()); + /// app.at("/"); + /// app.at("/hello"); + /// app.at("/message/{}"); + /// app.at("add_two/{num}"); + /// app.at("static/{path}*"); + /// app.at("single_page_app/*"); + /// ``` + /// + /// Notice that there is no fallback route matching, i.e. either a resource is a full match or + /// not, which means that the order of adding resources has no effect. + pub fn at<'a>(&'a mut self, path: &'a str) -> Route<'a, AppData> { + Route::new(&mut self.router, path.to_owned()) } /// Make this app into an `HttpService`. - pub fn into_http_service(mut self) -> Server { - self.router.apply_default_config(); + pub fn into_http_service(self) -> Server { Server { - data: self.data, router: Arc::new(self.router), - default_handler: Arc::new(self.default_handler), + data: Arc::new(self.data), + middleware: Arc::new(self.middleware), } } @@ -158,29 +141,20 @@ impl App { /// /// Blocks the calling thread indefinitely. #[cfg(feature = "hyper")] - pub fn serve(self) { - let configuration = self.get_item::().unwrap(); - let addr = format!("{}:{}", configuration.address, configuration.port) - .parse::() - .unwrap(); - + pub fn serve(self, addr: std::net::SocketAddr) { println!("Server is listening on: http://{}", addr); - - crate::serve::serve(self.into_http_service(), addr); + http_service_hyper::serve(self.into_http_service(), addr); } } #[derive(Clone)] -pub struct Server { - data: Data, - router: Arc>, - default_handler: Arc>, +pub struct Server { + router: Arc>, + data: Arc, + middleware: Arc>>>, } -impl HttpService for Server -where - Data: Clone + Send + Sync + 'static, -{ +impl HttpService for Server { type Connection = (); type ConnectionFuture = future::Ready>; type Fut = FutureObj<'static, Result>; @@ -190,58 +164,334 @@ where } fn respond(&self, _conn: &mut (), req: http_service::Request) -> Self::Fut { - let data = self.data.clone(); - let router = self.router.clone(); - let default_handler = self.default_handler.clone(); let path = req.uri().path().to_owned(); let method = req.method().to_owned(); + let router = self.router.clone(); + let middleware = self.middleware.clone(); + let data = self.data.clone(); - FutureObj::new(Box::new( - async move { - let RouteResult { - endpoint, - params, - middleware, - } = router.route(&path, &method, &default_handler); - - let ctx = RequestContext { - app_data: data, - req, - params, + box_async! { + let fut = { + let RouteResult { endpoint, params } = router.route(&path, method); + let cx = Context::new(data, req, params); + + let next = Next { endpoint, - next_middleware: middleware, + next_middleware: &middleware, }; - Ok(await!(ctx.next())) - }, - )) + + next.run(cx) + }; + + Ok(await!(fut)) + } } } -/// An extractor for accessing app data. -/// -/// Endpoints can use `AppData` to gain a handle to the data (of type `T`) originally injected into their app. -pub struct AppData(pub T); +#[cfg(test)] +mod tests { + use futures::{executor::block_on, future::FutureObj}; + + use super::*; + use crate::{middleware::RequestContext, AppData, Response}; -impl Deref for AppData { - type Target = T; - fn deref(&self) -> &T { - &self.0 + fn passthrough_middleware(ctx: RequestContext) -> FutureObj { + ctx.next() } -} -impl DerefMut for AppData { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 + + async fn simulate_request<'a, Data: Default + Clone + Send + Sync + 'static>( + router: &'a App, + path: &'a str, + method: &'a http::Method, + ) -> Option { + let default_handler = Arc::new(EndpointData { + endpoint: BoxedEndpoint::new(async || http::status::StatusCode::NOT_FOUND), + store: Store::new(), + }); + let RouteResult { + endpoint, + params, + middleware, + } = router.route(path, method, &default_handler); + + let data = Data::default(); + let req = http::Request::builder() + .method(method) + .body(http_service::Body::empty()) + .unwrap(); + + let ctx = RequestContext { + app_data: data, + req, + params, + endpoint, + next_middleware: middleware, + }; + let res = await!(ctx.next()); + Some(res.map(Into::into)) } -} -impl Extract for AppData { - type Fut = future::Ready>; - fn extract( - data: &mut T, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - future::ok(AppData(data.clone())) + fn route_middleware_count( + router: &App, + path: &str, + method: &http::Method, + ) -> Option { + let default_handler = Arc::new(EndpointData { + endpoint: BoxedEndpoint::new(async || http::status::StatusCode::NOT_FOUND), + store: Store::new(), + }); + let route_result = router.route(path, method, &default_handler); + Some(route_result.middleware.len()) + } + + #[test] + fn simple_static() { + let mut router: App<()> = App::new(); + router.at("/").get(async || "/"); + router.at("/foo").get(async || "/foo"); + router.at("/foo/bar").get(async || "/foo/bar"); + + for path in &["/", "/foo", "/foo/bar"] { + let res = + if let Some(res) = block_on(simulate_request(&router, path, &http::Method::GET)) { + res + } else { + panic!("Routing of path `{}` failed", path); + }; + let body = block_on(res.into_body().into_vec()).expect("Reading body should succeed"); + assert_eq!(&*body, path.as_bytes()); + } + } + + #[test] + fn nested_static() { + let mut router: App<()> = App::new(); + router.at("/a").get(async || "/a"); + router.at("/b").nest(|router| { + router.at("/").get(async || "/b"); + router.at("/a").get(async || "/b/a"); + router.at("/b").get(async || "/b/b"); + router.at("/c").nest(|router| { + router.at("/a").get(async || "/b/c/a"); + router.at("/b").get(async || "/b/c/b"); + }); + router.at("/d").get(async || "/b/d"); + }); + router.at("/a/a").nest(|router| { + router.at("/a").get(async || "/a/a/a"); + router.at("/b").get(async || "/a/a/b"); + }); + router.at("/a/b").nest(|router| { + router.at("/").get(async || "/a/b"); + }); + + for failing_path in &["/", "/a/a", "/a/b/a"] { + if let Some(res) = block_on(simulate_request(&router, failing_path, &http::Method::GET)) + { + if !res.status().is_client_error() { + panic!( + "Should have returned a client error when router cannot match with path {}", + failing_path + ); + } + } else { + panic!("Should have received a response from {}", failing_path); + }; + } + + for path in &[ + "/a", "/a/a/a", "/a/a/b", "/a/b", "/b", "/b/a", "/b/b", "/b/c/a", "/b/c/b", "/b/d", + ] { + let res = + if let Some(res) = block_on(simulate_request(&router, path, &http::Method::GET)) { + res + } else { + panic!("Routing of path `{}` failed", path); + }; + let body = block_on(res.into_body().into_vec()).expect("Reading body should succeed"); + assert_eq!(&*body, path.as_bytes()); + } + } + + #[test] + fn multiple_methods() { + let mut router: App<()> = App::new(); + router.at("/a").nest(|router| { + router.at("/b").get(async || "/a/b GET"); + }); + router.at("/a/b").post(async || "/a/b POST"); + + for (path, method) in &[("/a/b", http::Method::GET), ("/a/b", http::Method::POST)] { + let res = if let Some(res) = block_on(simulate_request(&router, path, &method)) { + res + } else { + panic!("Routing of {} `{}` failed", method, path); + }; + let body = block_on(res.into_body().into_vec()).expect("Reading body should succeed"); + assert_eq!(&*body, format!("{} {}", path, method).as_bytes()); + } + } + + #[test] + #[should_panic] + fn duplicate_endpoint_fails() { + let mut router: App<()> = App::new(); + router.at("/a").nest(|router| { + router.at("/b").get(async || ""); + }); // flattened into /a/b + router.at("/a/b").get(async || "duplicate"); + } + + #[test] + fn simple_middleware() { + let mut router: App<()> = App::new(); + router.middleware(passthrough_middleware); + router.at("/").get(async || "/"); + router.at("/b").nest(|router| { + router.at("/").get(async || "/b"); + router.middleware(passthrough_middleware); + }); + + assert_eq!( + route_middleware_count(&router, "/", &http::Method::GET), + Some(1) + ); + assert_eq!( + route_middleware_count(&router, "/b", &http::Method::GET), + Some(2) + ); + } + + #[test] + fn middleware_apply_order() { + #[derive(Default, Clone, Debug)] + struct Data(Vec); + struct Pusher(usize); + impl Middleware for Pusher { + fn handle<'a>(&'a self, mut ctx: RequestContext<'a, Data>) -> FutureObj<'a, Response> { + box_async! { + ctx.app_data.0.push(self.0); + await!(ctx.next()) + } + } + } + + // The order of endpoint and middleware does not matter + // The order of subrouter and middleware DOES matter + let mut router: App = App::new(); + router.middleware(Pusher(0)); + router.at("/").get(async move |data: AppData| { + if (data.0).0 == [0, 2] { + http::StatusCode::OK + } else { + http::StatusCode::INTERNAL_SERVER_ERROR + } + }); + router.at("/a").nest(|router| { + router.at("/").get(async move |data: AppData| { + if (data.0).0 == [0, 1, 2] { + http::StatusCode::OK + } else { + http::StatusCode::INTERNAL_SERVER_ERROR + } + }); + router.middleware(Pusher(1)); + }); + router.middleware(Pusher(2)); + router.at("/b").nest(|router| { + router.at("/").get(async move |data: AppData| { + if (data.0).0 == [0, 2, 1] { + http::StatusCode::OK + } else { + http::StatusCode::INTERNAL_SERVER_ERROR + } + }); + router.middleware(Pusher(1)); + }); + + for path in &["/", "/a", "/b"] { + let res = block_on(simulate_request(&router, path, &http::Method::GET)).unwrap(); + assert_eq!(res.status(), 200); + } + } + + #[test] + fn configuration() { + use crate::ExtractConfiguration; + async fn endpoint( + ExtractConfiguration(x): ExtractConfiguration<&'static str>, + ) -> &'static str { + x.unwrap() + } + + let mut router: App<()> = App::new(); + router.config("foo"); + router.at("/").get(endpoint); + router.at("/bar").get(endpoint).config("bar"); + router.apply_default_config(); // simulating App behavior + + let res = block_on(simulate_request(&router, "/", &http::Method::GET)).unwrap(); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"foo"); + + let res = block_on(simulate_request(&router, "/bar", &http::Method::GET)).unwrap(); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"bar"); + } + + #[test] + fn configuration_nested() { + use crate::ExtractConfiguration; + async fn endpoint( + ExtractConfiguration(x): ExtractConfiguration<&'static str>, + ) -> &'static str { + x.unwrap() + } + + let mut router: App<()> = App::new(); + router.config("foo"); + router.at("/").get(endpoint); + router.at("/bar").nest(|router| { + router.config("bar"); + router.at("/").get(endpoint); + router.at("/baz").get(endpoint).config("baz"); + }); + router.apply_default_config(); // simulating App behavior + + let res = block_on(simulate_request(&router, "/", &http::Method::GET)).unwrap(); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"foo"); + + let res = block_on(simulate_request(&router, "/bar", &http::Method::GET)).unwrap(); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"bar"); + + let res = block_on(simulate_request(&router, "/bar/baz", &http::Method::GET)).unwrap(); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"baz"); + } + + #[test] + fn configuration_order() { + use crate::ExtractConfiguration; + async fn endpoint( + ExtractConfiguration(x): ExtractConfiguration<&'static str>, + ) -> &'static str { + x.unwrap() + } + + let mut router: App<()> = App::new(); + router.at("/").get(endpoint); + router.config("foo"); // order does not matter + router.at("/bar").get(endpoint).config("bar"); + router.apply_default_config(); // simulating App behavior + + let res = block_on(simulate_request(&router, "/", &http::Method::GET)).unwrap(); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"foo"); + + let res = block_on(simulate_request(&router, "/bar", &http::Method::GET)).unwrap(); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"bar"); } } diff --git a/src/body.rs b/src/body.rs deleted file mode 100644 index a068abb40..000000000 --- a/src/body.rs +++ /dev/null @@ -1,351 +0,0 @@ -//! Types for working directly with the bodies of requests and responses. -//! -//! This module includes types like `Json`, which can be used to automatically (de)serialize bodies -//! using `serde_json`. -//! -//! # Examples -//! -//! Read/Write `Strings` and `Bytes` from/to bodies: -//! -//! ```rust, no_run -//! # #![feature(async_await, futures_api)] -//! use tide::body; -//! -//! async fn echo_string(msg: body::Str) -> String { -//! println!("String: {}", *msg); -//! format!("{}", *msg) -//! } -//! -//! async fn echo_string_lossy(msg: body::StrLossy) -> String { -//! println!("String: {}", *msg); -//! format!("{}", *msg) -//! } -//! -//! async fn echo_bytes(msg: body::Bytes) -> body::Bytes { -//! println!("Bytes: {:?}", *msg); -//! msg -//! } -//! -//! # fn main() { -//! # let mut app = tide::App::new(()); -//! # -//! app.at("/echo/string").post(echo_string); -//! app.at("/echo/string_lossy").post(echo_string_lossy); -//! app.at("/echo/bytes").post(echo_bytes); -//! -//! # app.serve(); -//! # } -//! -//! ``` -//! -//! Using `serde_json` to automatically (de)serialize bodies into/from structs: -//! -//! ```rust, no_run -//! # #![feature(async_await, futures_api)] -//! #[macro_use] -//! extern crate serde_derive; -//! use tide::body; -//! -//! #[derive(Serialize, Deserialize, Clone, Debug)] -//! struct Message { -//! author: Option, -//! contents: String, -//! } -//! -//! async fn echo_json(msg: body::Json) -> body::Json { -//! println!("JSON: {:?}", *msg); -//! msg -//! } -//! -//! async fn echo_form(msg: body::Form) -> body::Form { -//! println!("Form: {:?}", *msg); -//! msg -//! } -//! -//! # fn main() { -//! # let mut app = tide::App::new(()); -//! # -//! app.at("/echo/json").post(echo_json); -//! app.at("/echo/form").post(echo_form); -//! # -//! # app.serve(); -//! # } -//! -//! ``` -//! -use futures::future::FutureObj; -use http::status::StatusCode; -use http_service::Body; -use multipart::server::Multipart; -use std::io::Cursor; -use std::ops::{Deref, DerefMut}; - -use crate::{configuration::Store, Extract, IntoResponse, Request, Response, RouteMatch}; - -// Small utility function to return a stamped error when we cannot parse a request body -fn mk_err(_: T) -> Response { - StatusCode::BAD_REQUEST.into_response() -} - -/// A wrapper for multipart form -/// -/// This type is useable as an extractor (argument to an endpoint) for getting -/// a Multipart type defined in the multipart crate -pub struct MultipartForm(pub Multipart>>); - -impl Extract for MultipartForm { - // Note: cannot use `existential type` here due to ICE - type Fut = FutureObj<'static, Result>; - - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - // https://stackoverflow.com/questions/43424982/how-to-parse-multipart-forms-using-abonander-multipart-with-rocket - - const BOUNDARY: &str = "boundary="; - let boundary = req.headers().get("content-type").and_then(|ct| { - let ct = ct.to_str().ok()?; - let idx = ct.find(BOUNDARY)?; - Some(ct[idx + BOUNDARY.len()..].to_string()) - }); - - let body = std::mem::replace(req.body_mut(), Body::empty()); - - FutureObj::new(Box::new( - async move { - let body = await!(body.into_vec()).map_err(mk_err)?; - let boundary = boundary.ok_or(()).map_err(mk_err)?; - let mp = Multipart::with_body(Cursor::new(body), boundary); - Ok(MultipartForm(mp)) - }, - )) - } -} - -impl Deref for MultipartForm { - type Target = Multipart>>; - fn deref(&self) -> &Multipart>> { - &self.0 - } -} - -impl DerefMut for MultipartForm { - fn deref_mut(&mut self) -> &mut Multipart>> { - &mut self.0 - } -} - -/// A wrapper for json (de)serialization of bodies. -/// -/// This type is usable both as an extractor (argument to an endpoint) and as a response -/// (return value from an endpoint). -pub struct Json(pub T); - -impl Extract for Json { - // Note: cannot use `existential type` here due to ICE - type Fut = FutureObj<'static, Result>; - - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - let body = std::mem::replace(req.body_mut(), Body::empty()); - FutureObj::new(Box::new( - async move { - let body = await!(body.into_vec()).map_err(mk_err)?; - let json: T = serde_json::from_slice(&body).map_err(mk_err)?; - Ok(Json(json)) - }, - )) - } -} - -impl IntoResponse for Json { - fn into_response(self) -> Response { - // TODO: think about how to handle errors - http::Response::builder() - .status(http::status::StatusCode::OK) - .header("Content-Type", "application/json") - .body(Body::from(serde_json::to_vec(&self.0).unwrap())) - .unwrap() - } -} - -impl Deref for Json { - type Target = T; - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Json { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -/// A wrapper for form encoded (application/x-www-form-urlencoded) (de)serialization of bodies. -/// -/// This type is usable both as an extractor (argument to an endpoint) and as a response -/// (return value from an endpoint), though returning a response with form data is uncommon -/// and probably not good practice. -pub struct Form(pub T); - -impl Extract for Form { - // Note: cannot use `existential type` here due to ICE - type Fut = FutureObj<'static, Result>; - - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - let body = std::mem::replace(req.body_mut(), Body::empty()); - FutureObj::new(Box::new( - async move { - let body = await!(body.into_vec()).map_err(mk_err)?; - let data: T = serde_qs::from_bytes(&body).map_err(mk_err)?; - Ok(Form(data)) - }, - )) - } -} - -impl IntoResponse for Form { - fn into_response(self) -> Response { - // TODO: think about how to handle errors - http::Response::builder() - .status(http::status::StatusCode::OK) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(Body::from( - serde_qs::to_string(&self.0).unwrap().into_bytes(), - )) - .unwrap() - } -} - -impl Deref for Form { - type Target = T; - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Form { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -pub struct Str(pub String); - -impl Extract for Str { - type Fut = FutureObj<'static, Result>; - - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - let body = std::mem::replace(req.body_mut(), Body::empty()); - - FutureObj::new(Box::new( - async move { - let body = await!(body.into_vec()).map_err(mk_err)?; - let string = String::from_utf8(body).map_err(mk_err)?; - Ok(Str(string)) - }, - )) - } -} - -impl Deref for Str { - type Target = String; - fn deref(&self) -> &String { - &self.0 - } -} - -impl DerefMut for Str { - fn deref_mut(&mut self) -> &mut String { - &mut self.0 - } -} - -pub struct StrLossy(pub String); - -impl Extract for StrLossy { - type Fut = FutureObj<'static, Result>; - - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - let body = std::mem::replace(req.body_mut(), Body::empty()); - - FutureObj::new(Box::new( - async move { - let body = await!(body.into_vec()).map_err(mk_err)?; - let string = String::from_utf8_lossy(&body).to_string(); - Ok(StrLossy(string)) - }, - )) - } -} - -impl Deref for StrLossy { - type Target = String; - fn deref(&self) -> &String { - &self.0 - } -} - -impl DerefMut for StrLossy { - fn deref_mut(&mut self) -> &mut String { - &mut self.0 - } -} - -pub struct Bytes(pub Vec); - -impl Extract for Bytes { - type Fut = FutureObj<'static, Result>; - - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - let body = std::mem::replace(req.body_mut(), Body::empty()); - - FutureObj::new(Box::new( - async move { - let body = await!(body.into_vec()).map_err(mk_err)?; - Ok(Bytes(body)) - }, - )) - } -} - -impl Deref for Bytes { - type Target = Vec; - fn deref(&self) -> &Vec { - &self.0 - } -} - -impl DerefMut for Bytes { - fn deref_mut(&mut self) -> &mut Vec { - &mut self.0 - } -} diff --git a/src/configuration/default_config.rs b/src/configuration/default_config.rs deleted file mode 100644 index fd75c6f68..000000000 --- a/src/configuration/default_config.rs +++ /dev/null @@ -1,79 +0,0 @@ -/// What environment are we running in? -#[derive(Debug, Clone)] -pub enum Environment { - Development, - Staging, - Production, -} - -/// Holds the default configuration for the App. -/// -/// Only the one that is applied to the top-level router will be regarded. Overriding this item in -/// resource paths or subrouters has no effect. -#[derive(Debug, Clone)] -pub struct Configuration { - pub env: Environment, - pub address: String, - pub port: u16, -} - -pub struct ConfigurationBuilder { - pub env: Environment, - pub address: String, - pub port: u16, -} - -impl Default for Configuration { - fn default() -> Self { - Self { - env: Environment::Development, - address: "127.0.0.1".to_owned(), - port: 8181, - } - } -} - -impl Configuration { - pub fn build() -> ConfigurationBuilder { - ConfigurationBuilder::default() - } -} - -impl Default for ConfigurationBuilder { - fn default() -> Self { - let config = Configuration::default(); - - Self { - env: config.env, - address: config.address, - port: config.port, - } - } -} - -impl ConfigurationBuilder { - pub fn env(mut self, env: Environment) -> Self { - self.env = env; - self - } - - pub fn address>(mut self, address: A) -> Self { - self.address = address.into(); - self - } - - pub fn port(mut self, port: u16) -> Self { - self.port = port; - self - } - - pub fn finalize(self) -> Configuration { - let mut config = Configuration::default(); - - config.port = self.port; - config.address = self.address; - config.env = self.env; - - config - } -} diff --git a/src/configuration/mod.rs b/src/configuration/mod.rs deleted file mode 100644 index 0dcc23ec5..000000000 --- a/src/configuration/mod.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Types for managing and extracting configuration. - -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::fmt::{self, Debug}; - -use futures::future::FutureObj; - -use crate::{Extract, Request, Response, RouteMatch}; - -mod default_config; - -pub use self::default_config::{Configuration, ConfigurationBuilder, Environment}; - -trait StoreItem: Any + Send + Sync { - fn clone_any(&self) -> Box; - fn as_dyn_any(&self) -> &(dyn Any + Send + Sync); - fn as_dyn_any_mut(&mut self) -> &mut (dyn Any + Send + Sync); - fn fmt_debug(&self, fmt: &mut fmt::Formatter) -> fmt::Result; -} - -impl StoreItem for T -where - T: Any + Debug + Clone + Send + Sync, -{ - fn clone_any(&self) -> Box { - Box::new(self.clone()) - } - - fn as_dyn_any(&self) -> &(dyn Any + Send + Sync) { - self - } - - fn as_dyn_any_mut(&mut self) -> &mut (dyn Any + Send + Sync) { - self - } - - fn fmt_debug(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - self.fmt(fmt) - } -} - -impl Clone for Box { - fn clone(&self) -> Box { - (&**self).clone_any() - } -} - -impl Debug for Box { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - (&**self).fmt_debug(fmt) - } -} - -/// A cloneable typemap for saving per-endpoint configuration. -/// -/// Store is mostly managed by `App` and `Router`, so this is normally not used directly. -#[derive(Clone)] -pub struct Store(HashMap>); - -impl Debug for Store { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt.debug_set().entries(self.0.values()).finish() - } -} - -impl Store { - pub(crate) fn new() -> Self { - Store(HashMap::new()) - } - - pub(crate) fn merge(&mut self, base: &Store) { - let overlay = std::mem::replace(&mut self.0, base.0.clone()); - self.0.extend(overlay); - } - - /// Retrieve the configuration item of given type, returning `None` if it is not found. - pub fn read(&self) -> Option<&T> { - let id = TypeId::of::(); - self.0 - .get(&id) - .and_then(|v| (**v).as_dyn_any().downcast_ref::()) - } - - /// Save the given configuration item. - pub fn write(&mut self, value: T) { - let id = TypeId::of::(); - self.0.insert(id, Box::new(value) as Box); - } -} - -/// An extractor for reading configuration from endpoints. -/// -/// It will try to retrieve the given configuration item. If it is not set, the extracted value -/// will be `None`. -pub struct ExtractConfiguration(pub Option); - -impl Extract for ExtractConfiguration -where - S: 'static, - T: Any + Debug + Clone + Send + Sync + 'static, -{ - type Fut = FutureObj<'static, Result>; - - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - // The return type here is Option, but the return type of the result of the future is - // Result, Response>, so rustc can infer that K == T, so we do not - // need config.read::().cloned() - let store_item = store.read().cloned(); - FutureObj::new(Box::new( - async move { Ok(ExtractConfiguration(store_item)) }, - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn store_read_write() { - let mut store = Store::new(); - assert_eq!(store.read::(), None); - assert_eq!(store.read::(), None); - store.write(42usize); - store.write(-3isize); - assert_eq!(store.read::(), Some(&42)); - assert_eq!(store.read::(), Some(&-3)); - store.write(3usize); - assert_eq!(store.read::(), Some(&3)); - } - - #[test] - fn store_clone() { - let mut store = Store::new(); - store.write(42usize); - store.write(String::from("foo")); - - let mut new_store = store.clone(); - new_store.write(3usize); - new_store.write(4u32); - - assert_eq!(store.read::(), Some(&42)); - assert_eq!(store.read::(), None); - assert_eq!(store.read::(), Some(&"foo".into())); - - assert_eq!(new_store.read::(), Some(&3)); - assert_eq!(new_store.read::(), Some(&4)); - assert_eq!(new_store.read::(), Some(&"foo".into())); - } -} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 000000000..c4d24fc2f --- /dev/null +++ b/src/context.rs @@ -0,0 +1,87 @@ +use http::{Extensions, HeaderMap, Method, Uri, Version}; +use http_service::Body; +use route_recognizer::Params; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub struct Context { + app_data: Arc, + context_data: Arc, +} + +struct ContextData { + meta: http::request::Parts, + body: Mutex, + extensions: Mutex, + route_params: Params, +} + +impl Context { + pub(crate) fn new( + app_data: Arc, + req: http::Request, + route_params: Params, + ) -> Context { + let (meta, body) = req.into_parts(); + Context { + app_data, + context_data: Arc::new(ContextData { + meta, + body: Mutex::new(body), + extensions: Mutex::new(Extensions::new()), + route_params, + }), + } + } + + pub fn method(&self) -> &Method { + &self.context_data.meta.method + } + + pub fn uri(&self) -> &Uri { + &self.context_data.meta.uri + } + + pub fn version(&self) -> Version { + self.context_data.meta.version + } + + pub fn headers(&self) -> &HeaderMap { + &self.context_data.meta.headers + } + + pub fn app_data(&self) -> &AppData { + &self.app_data + } + + pub fn route_param(&self, key: &str) -> Option<&str> { + self.context_data.route_params.find(key) + } + + pub async fn body_bytes(&mut self) -> std::io::Result> { + let body = self.replace_body(Body::empty()); + await!(body.into_vec()) + } + + pub async fn body_string(&mut self) -> std::io::Result { + let body_bytes = await!(self.body_bytes())?; + Ok(String::from_utf8(body_bytes).map_err(|_| std::io::ErrorKind::InvalidData)?) + } + + pub async fn body_json(&mut self) -> std::io::Result { + let body_bytes = await!(self.body_bytes())?; + Ok(serde_json::from_slice(&body_bytes).map_err(|_| std::io::ErrorKind::InvalidData)?) + } + + pub fn replace_body(&mut self, body: Body) -> Body { + std::mem::replace(&mut self.context_data.body.lock().unwrap(), body) + } + + pub fn insert_extension(&mut self, ext: T) -> Option { + self.context_data.extensions.lock().unwrap().insert(ext) + } + + pub fn remove_extension(&mut self) -> Option { + self.context_data.extensions.lock().unwrap().remove() + } +} diff --git a/src/cookies.rs b/src/cookies.rs index 7d3cca79c..6649228a9 100644 --- a/src/cookies.rs +++ b/src/cookies.rs @@ -1,44 +1,34 @@ use cookie::{Cookie, CookieJar, ParseError}; -use futures::future; -use crate::{configuration::Store, response::IntoResponse, Extract, Request, Response, RouteMatch}; +use crate::Context; /// A representation of cookies which wraps `CookieJar` from `cookie` crate /// /// Currently this only exposes getting cookie by name but future enhancements might allow more -/// operation. `Cookies` implements`Extract` so that handler methods can have a `Cookies` parameter. -/// -#[derive(Clone, Debug)] -pub struct Cookies { +/// operations +struct CookieData { content: CookieJar, } -impl Cookies { +/// An extension to `Context` that provides cached access to cookies +pub trait ExtractCookies { /// returns a `Cookie` by name of the cookie - #[inline] - pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { - self.content.get(name) - } + fn cookie(&mut self, name: &str) -> Option>; } -impl Extract for Cookies { - type Fut = future::Ready>; - - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - let cookie_jar = match req.headers().get("Cookie") { - Some(raw_cookies) => parse_from_header(raw_cookies.to_str().unwrap()), - _ => Ok(CookieJar::new()), - }; - let resp = cookie_jar - .map(|c| Cookies { content: c }) - .map_err(|_e| http::status::StatusCode::BAD_REQUEST.into_response()); - - future::ready(resp) +impl ExtractCookies for Context { + fn cookie(&mut self, name: &str) -> Option> { + let cookie_data = self.remove_extension().unwrap_or_else(|| CookieData { + content: self + .headers() + .get("Cookie") + .and_then(|raw| parse_from_header(raw.to_str().unwrap()).ok()) + .unwrap_or(CookieJar::new()), + }); + let cookie = cookie_data.content.get(name).cloned(); + self.insert_extension(cookie_data); + + cookie } } diff --git a/src/endpoint.rs b/src/endpoint.rs index 6b3c9295c..118c84d9a 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -1,8 +1,7 @@ use futures::future::{Future, FutureObj}; +use std::pin::Pin; -use crate::{ - configuration::Store, extract::Extract, head::Head, IntoResponse, Request, Response, RouteMatch, -}; +use crate::{response::IntoResponse, Context, Response}; /// The raw representation of an endpoint. /// @@ -62,112 +61,42 @@ use crate::{ /// /// See [`body`](body/index.html) module for examples of how to work with request and response bodies. /// -pub trait Endpoint: Send + Sync + 'static { +pub trait Endpoint: Send + Sync + 'static { /// The async result of `call`. type Fut: Future + Send + 'static; - /// Invoke the endpoint on the given request and app data handle. - fn call( - &self, - data: Data, - req: Request, - params: Option>, - store: &Store, - ) -> Self::Fut; + /// Invoke the endpoint + fn call(&self, cx: Context) -> Self::Fut; } -type BoxedEndpointFn = - dyn Fn(Data, Request, Option, &Store) -> FutureObj<'static, Response> + Send + Sync; +pub(crate) type DynEndpoint = + dyn (Fn(Context) -> FutureObj<'static, Response>) + 'static + Send + Sync; -pub(crate) struct BoxedEndpoint { - endpoint: Box>, -} - -impl BoxedEndpoint { - pub fn new(ep: T) -> BoxedEndpoint - where - T: Endpoint, - { - BoxedEndpoint { - endpoint: Box::new(move |data, request, params, store| { - FutureObj::new(Box::new(ep.call(data, request, params, store))) - }), - } - } - - pub fn call( - &self, - data: Data, - req: Request, - params: Option>, - store: &Store, - ) -> FutureObj<'static, Response> { - (self.endpoint)(data, req, params, store) +impl Endpoint for F +where + F: Fn(Context) -> Fut, + Fut: Future + Send + 'static, + Fut::Output: IntoResponse, +{ + type Fut = ResponseWrapper; + fn call(&self, cx: Context) -> Self::Fut { + ResponseWrapper { fut: (self)(cx) } } } -/// A marker type used for the (phantom) `Kind` parameter in endpoints. -#[doc(hidden)] -pub struct Ty(T); - -macro_rules! call_f { - ($head_ty:ty; ($f:ident, $head:ident); $($X:ident),*) => { - $f($head.clone(), $($X),*) - }; - (($f:ident, $head:ident); $($X:ident),*) => { - $f($($X),*) - }; +pub struct ResponseWrapper { + fut: F, } -macro_rules! end_point_impl_raw { - ($([$head:ty])* $($X:ident),*) => { - impl Endpoint, $($head,)* $(Ty<$X>),*)> for T - where - T: Send + Sync + Clone + 'static + Fn($($head,)* $($X),*) -> Fut, - Data: Clone + Send + Sync + 'static, - Fut: Future + Send + 'static, - Fut::Output: IntoResponse, - $( - $X: Extract - ),* - { - type Fut = FutureObj<'static, Response>; +impl Future for ResponseWrapper +where + F: Future, + F::Output: IntoResponse, +{ + type Output = Response; - #[allow(unused_mut, non_snake_case)] - fn call(&self, mut data: Data, mut req: Request, params: Option>, store: &Store) -> Self::Fut { - let f = self.clone(); - $(let $X = $X::extract(&mut data, &mut req, ¶ms, store);)* - FutureObj::new(Box::new(async move { - let (parts, _) = req.into_parts(); - let head = Head::from(parts); - $(let $X = match await!($X) { - Ok(x) => x, - Err(resp) => return resp, - };)* - let res = await!(call_f!($($head;)* (f, head); $($X),*)); - - res.into_response() - })) - } - } - }; -} - -macro_rules! end_point_impl { - ($($X:ident),*) => { - end_point_impl_raw!([Head] $($X),*); - end_point_impl_raw!($($X),*); + fn poll(self: Pin<&mut Self>, waker: &std::task::Waker) -> std::task::Poll { + let inner = unsafe { self.map_unchecked_mut(|wrapper| &mut wrapper.fut) }; + inner.poll(waker).map(IntoResponse::into_response) } } - -end_point_impl!(); -end_point_impl!(T0); -end_point_impl!(T0, T1); -end_point_impl!(T0, T1, T2); -end_point_impl!(T0, T1, T2, T3); -end_point_impl!(T0, T1, T2, T3, T4); -end_point_impl!(T0, T1, T2, T3, T4, T5); -end_point_impl!(T0, T1, T2, T3, T4, T5, T6); -end_point_impl!(T0, T1, T2, T3, T4, T5, T6, T7); -end_point_impl!(T0, T1, T2, T3, T4, T5, T6, T7, T8); -end_point_impl!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9); diff --git a/src/extract.rs b/src/extract.rs deleted file mode 100644 index d0988a451..000000000 --- a/src/extract.rs +++ /dev/null @@ -1,20 +0,0 @@ -use futures::prelude::*; - -use crate::{configuration::Store, Request, Response, RouteMatch}; - -/// An extractor for an app with `Data` -pub trait Extract: Send + Sized + 'static { - /// The async result of `extract`. - /// - /// The `Err` case represents that the endpoint should not be invoked, but - /// rather the given response should be returned immediately. - type Fut: Future> + Send + 'static; - - /// Attempt to extract a value from the given request. - fn extract( - data: &mut Data, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut; -} diff --git a/src/forms.rs b/src/forms.rs new file mode 100644 index 000000000..8af8d437a --- /dev/null +++ b/src/forms.rs @@ -0,0 +1,48 @@ +use futures::future::FutureObj; +use http_service::Body; +use multipart::server::Multipart; +use std::io::Cursor; + +use crate::{Context, Response}; + +pub trait ExtractForms { + fn body_form(&mut self) -> crate::BoxTryFuture; + + fn body_multipart(&mut self) -> crate::BoxTryFuture>>>; +} + +impl ExtractForms for Context { + fn body_form(&mut self) -> crate::BoxTryFuture { + let body = self.replace_body(Body::empty()); + box_async! { + let body = await!(body.into_vec())?; + Ok(serde_qs::from_bytes(&body).map_err(|e| format!("form deserialization failed: {}", e))?) + } + } + + fn body_multipart(&mut self) -> crate::BoxTryFuture>>> { + const BOUNDARY: &str = "boundary="; + let boundary = self.headers().get("content-type").and_then(|ct| { + let ct = ct.to_str().ok()?; + let idx = ct.find(BOUNDARY)?; + Some(ct[idx + BOUNDARY.len()..].to_string()) + }); + + let body = self.replace_body(Body::empty()); + + box_async! { + let body = await!(body.into_vec())?; + let boundary = boundary.ok_or("no boundary found")?; + Ok(Multipart::with_body(Cursor::new(body), boundary)) + } + } +} + +pub fn form(t: T) -> Response { + // TODO: think about how to handle errors + http::Response::builder() + .status(http::status::StatusCode::OK) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(Body::from(serde_qs::to_string(&t).unwrap().into_bytes())) + .unwrap() +} diff --git a/src/head.rs b/src/head.rs deleted file mode 100644 index b96e9b530..000000000 --- a/src/head.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Types related to the headers (and metadata) of a request. -//! -//! This module includes extractors like `Path` that endpoints can use to -//! automatically parse out information from a request. - -use futures::future; -use std::ops::{Deref, DerefMut}; -use std::sync::Arc; - -use crate::{configuration::Store, Extract, IntoResponse, Request, Response, RouteMatch}; - -/// Header and metadata for a request. -/// -/// Essentially an immutable, cheaply clonable version of `http::request::Parts`. -#[derive(Clone)] -pub struct Head { - inner: Arc, -} - -impl From for Head { - fn from(parts: http::request::Parts) -> Self { - Self { - inner: Arc::new(parts), - } - } -} - -impl Head { - /// The full URI for this request - pub fn uri(&self) -> &http::Uri { - &self.inner.uri - } - - /// The path portion of this request - pub fn path(&self) -> &str { - self.uri().path() - } - - /// The query portion of this request - pub fn query(&self) -> Option<&str> { - self.uri().query() - } - - /// The HTTP method being invoked - pub fn method(&self) -> &http::Method { - &self.inner.method - } - - /// The HTTP headers - pub fn headers(&self) -> &http::header::HeaderMap { - &self.inner.headers - } -} - -/// An extractor for path segments. -/// -/// Routes can use wildcard path segments (`{}`), which are then extracted by the endpoint using -/// this `Path` extractor. Each `Path` argument to an extractor parses the next wildcard segment -/// as type `T`, failing with a `NOT_FOUND` response if the segment fails to parse. -/// -/// # Examples -/// -/// Extracting a path segment with: -/// -/// ```rust, no_run -/// # #![feature(async_await, futures_api)] -/// use tide::head; -/// -/// async fn path_segment(head::Path(s): head::Path) -> String { -/// println!("read segment: {}", s); -/// s -/// } -/// -/// fn main() { -/// let mut app = tide::App::new(()); -/// app.at("/path/{}").get(path_segment); -/// app.serve() -/// } -/// ``` -/// -pub struct Path(pub T); - -impl Deref for Path { - type Target = T; - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Path { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -/// A key for storing the current segment match in a request's `extensions` -struct PathIdx(usize); - -impl Extract for Path { - type Fut = future::Ready>; - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - let &PathIdx(i) = req.extensions().get::().unwrap_or(&PathIdx(0)); - req.extensions_mut().insert(PathIdx(i + 1)); - match params { - Some(params) => match params.vec[i].parse() { - Ok(t) => future::ok(Path(t)), - Err(_) => future::err(http::status::StatusCode::BAD_REQUEST.into_response()), - }, - None => future::err(http::status::StatusCode::INTERNAL_SERVER_ERROR.into_response()), - } - } -} - -/// A trait providing the name of a named url segment -pub trait NamedSegment: Send + 'static + std::str::FromStr { - const NAME: &'static str; -} - -/// An extractor for named path segments -/// -/// Allows routes to access named path segments (`{foo}`). Each `Named` extracts a single -/// segment. `T` must implement the `NamedSegment` trait - to provide the segment name - and the -/// FromStr trait. Fails with a `BAD_REQUEST` response if the segment is not found, fails to -/// parse or if multiple identically named segments exist. -/// -/// # Examples -/// -/// Extracting a `Number` from a named path segment with: -/// -/// ```rust, no_run -/// # #![feature(async_await, futures_api)] -/// use tide::head; -/// use tide::head::{Named, NamedSegment}; -/// -/// struct Number(i32); -/// -/// impl NamedSegment for Number { -/// const NAME: &'static str = "num"; -/// } -/// -/// impl std::str::FromStr for Number { -/// type Err = std::num::ParseIntError; -/// -/// fn from_str(s: &str) -> Result { -/// s.parse().map(|num| Number(num)) -/// } -/// } -/// -/// async fn named_segments(Named(number): Named) -> String { -/// let Number(num) = number; -/// format!("number: {}", num) -/// } -/// -/// fn main() { -/// let mut app = tide::App::new(()); -/// app.at("/path_named/{num}").get(named_segments); -/// app.serve() -/// } -/// ``` -/// -pub struct Named(pub T); - -impl Deref for Named { - type Target = T; - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Named { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl Extract for Named { - type Fut = future::Ready>; - - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - match params { - Some(params) => params - .map - .get(T::NAME) - .and_then(|segment| segment.parse().ok()) - .map_or( - future::err(http::status::StatusCode::BAD_REQUEST.into_response()), - |t| future::ok(Named(t)), - ), - None => future::err(http::status::StatusCode::BAD_REQUEST.into_response()), - } - } -} - -/// An extractor for query string in URL -/// -pub struct UrlQuery(pub T); - -impl Extract for UrlQuery -where - T: Send + std::str::FromStr + 'static, - S: 'static, -{ - type Fut = future::Ready>; - fn extract( - data: &mut S, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - req.uri().query().and_then(|q| q.parse().ok()).map_or( - future::err(http::status::StatusCode::BAD_REQUEST.into_response()), - |q| future::ok(UrlQuery(q)), - ) - } -} diff --git a/src/lib.rs b/src/lib.rs index baa99393d..382090736 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,29 +11,32 @@ //! The [`App`](struct.App.html) docs are a good place to get started. //! //! + +use futures::future::FutureObj; + +macro_rules! box_async { + {$($t:tt)*} => { + FutureObj::new(Box::new(async move { $($t)* })) + }; +} + mod app; -pub mod body; -pub mod configuration; -mod cookies; +mod context; +pub mod cookies; mod endpoint; -mod extract; -pub mod head; +pub mod forms; pub mod middleware; -mod request; -mod response; +pub mod response; +mod route; mod router; -#[cfg(feature = "hyper")] -mod serve; pub use crate::{ - app::{App, AppData, Server}, - configuration::ExtractConfiguration, - cookies::Cookies, + app::{App, Server}, + context::Context, endpoint::Endpoint, - extract::Extract, - middleware::Middleware, - request::{Compute, Computed, Request}, - response::{IntoResponse, Response}, - router::{Resource, Router}, + response::Response, + route::Route, }; -pub use path_table::RouteMatch; + +pub type Result = std::result::Result>; +pub type BoxTryFuture = FutureObj<'static, Result>; diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index e5c42d139..d3f7eaa20 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -5,7 +5,10 @@ use http::{ HeaderMap, HttpTryFrom, }; -use crate::{middleware::RequestContext, Middleware, Response}; +use crate::{ + middleware::{Middleware, Next}, + Context, Response, +}; #[derive(Clone, Default)] pub struct DefaultHeaders { @@ -33,18 +36,16 @@ impl DefaultHeaders { } } -impl Middleware for DefaultHeaders { - fn handle<'a>(&'a self, ctx: RequestContext<'a, Data>) -> FutureObj<'a, Response> { - FutureObj::new(Box::new( - async move { - let mut res = await!(ctx.next()); - - let headers = res.headers_mut(); - for (key, value) in self.headers.iter() { - headers.entry(key).unwrap().or_insert_with(|| value.clone()); - } - res - }, - )) +impl Middleware for DefaultHeaders { + fn handle<'a>(&'a self, cx: Context, next: Next<'a, Data>) -> FutureObj<'a, Response> { + box_async! { + let mut res = await!(next.run(cx)); + + let headers = res.headers_mut(); + for (key, value) in self.headers.iter() { + headers.entry(key).unwrap().or_insert_with(|| value.clone()); + } + res + } } } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 249e31567..ca7066f31 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -4,12 +4,13 @@ use slog_term; use futures::future::FutureObj; -use crate::{middleware::RequestContext, Middleware, Response}; +use crate::{ + middleware::{Middleware, Next}, + Context, Response, +}; /// Root logger for Tide. Wraps over logger provided by slog.SimpleLogger -/// -/// Only used internally for now. -pub(crate) struct RootLogger { +pub struct RootLogger { // drain: dyn slog::Drain, inner_logger: slog::Logger, } @@ -27,18 +28,16 @@ impl RootLogger { /// Stores information during request phase and logs information once the response /// is generated. -impl Middleware for RootLogger { - fn handle<'a>(&'a self, ctx: RequestContext<'a, Data>) -> FutureObj<'a, Response> { - FutureObj::new(Box::new( - async move { - let path = ctx.req.uri().path().to_owned(); - let method = ctx.req.method().as_str().to_owned(); +impl Middleware for RootLogger { + fn handle<'a>(&'a self, cx: Context, next: Next<'a, Data>) -> FutureObj<'a, Response> { + box_async! { + let path = cx.uri().path().to_owned(); + let method = cx.method().as_str().to_owned(); - let res = await!(ctx.next()); - let status = res.status(); - info!(self.inner_logger, "{} {} {}", method, path, status.as_str()); - res - }, - )) + let res = await!(next.run(cx)); + let status = res.status(); + info!(self.inner_logger, "{} {} {}", method, path, status.as_str()); + res + } } } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 03434509b..08c861ddc 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,65 +1,35 @@ -use std::any::Any; -use std::fmt::Debug; -use std::sync::Arc; - use futures::future::FutureObj; +use std::sync::Arc; -use crate::{configuration::Store, router::EndpointData, Request, Response, RouteMatch}; +use crate::{endpoint::DynEndpoint, Context, Response}; mod default_headers; -pub mod logger; +mod logger; -pub use self::default_headers::DefaultHeaders; +pub use self::{default_headers::DefaultHeaders, logger::RootLogger}; /// Middleware that wraps around remaining middleware chain. -pub trait Middleware: Send + Sync { +pub trait Middleware: 'static + Send + Sync { /// Asynchronously handle the request, and return a response. - fn handle<'a>(&'a self, ctx: RequestContext<'a, Data>) -> FutureObj<'a, Response>; -} - -impl Middleware for F -where - F: Send + Sync + Fn(RequestContext) -> FutureObj, -{ - fn handle<'a>(&'a self, ctx: RequestContext<'a, Data>) -> FutureObj<'a, Response> { - (self)(ctx) - } + fn handle<'a>( + &'a self, + cx: Context, + next: Next<'a, AppData>, + ) -> FutureObj<'a, Response>; } -pub struct RequestContext<'a, Data> { - pub app_data: Data, - pub req: Request, - pub params: Option>, - pub(crate) endpoint: &'a EndpointData, - pub(crate) next_middleware: &'a [Arc + Send + Sync>], +pub struct Next<'a, AppData> { + pub(crate) endpoint: &'a DynEndpoint, + pub(crate) next_middleware: &'a [Arc>], } -impl<'a, Data: Clone + Send> RequestContext<'a, Data> { - /// Get a configuration item of given type from the endpoint. - pub fn get_item(&self) -> Option<&T> { - self.endpoint.store.read::() - } - - /// Get the configuration store for this request. - /// - /// This is for debug purposes. `Store` implements `Debug`, so the store can be inspected using - /// `{:?}` formatter. - pub fn store(&self) -> &Store { - &self.endpoint.store - } - - /// Consume this context, and run remaining middleware chain to completion. - pub fn next(mut self) -> FutureObj<'a, Response> { +impl<'a, AppData: 'static> Next<'a, AppData> { + pub fn run(mut self, cx: Context) -> FutureObj<'a, Response> { if let Some((current, next)) = self.next_middleware.split_first() { self.next_middleware = next; - current.handle(self) + current.handle(cx, self) } else { - FutureObj::new(Box::new(self.endpoint.endpoint.call( - self.app_data.clone(), - self.req, - self.params, - &self.endpoint.store, - ))) + (self.endpoint)(cx) } } } diff --git a/src/request.rs b/src/request.rs deleted file mode 100644 index 6adae8dfc..000000000 --- a/src/request.rs +++ /dev/null @@ -1,65 +0,0 @@ -use futures::future; -use http_service::Body; -use std::ops::{Deref, DerefMut}; - -use crate::{configuration::Store, Extract, Response, RouteMatch}; - -/// An HTTP request. -/// -/// A convenient alias for the `http::Request` type, using Tide's `Body`. -pub type Request = http::Request; - -/// A value that can be computed on-demand from a request. -pub trait Compute: 'static + Sync + Send + Clone + Sized { - /// Compute the value directly from the given request. - fn compute_fresh(req: &mut Request) -> Self; - - /// Compute the value, or return a copy if it has already been computed for this request. - fn compute(req: &mut Request) -> Self { - if req.extensions().get::>().is_none() { - let t = Self::compute_fresh(req); - req.extensions_mut().insert(ComputedMarker(t)); - } - - req.extensions() - .get::>() - .unwrap() - .0 - .clone() - } -} - -/// A private marker to ensure that computed values cannot be accessed directly through `extensions` -struct ComputedMarker(T); - -/// An extractor for computed values. -/// -/// Endpoints can use this extractor to automatically compute values for a request, and re-use cached -/// results if those values have been computed previously (e.g. in some middleware). -#[derive(Clone)] -pub struct Computed(pub T); - -impl Deref for Computed { - type Target = T; - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Computed { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl Extract for Computed { - type Fut = future::Ready>; - fn extract( - data: &mut Data, - req: &mut Request, - params: &Option>, - store: &Store, - ) -> Self::Fut { - future::ok(Computed(T::compute(req))) - } -} diff --git a/src/response.rs b/src/response.rs index d424fcccd..e043cd685 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,11 +1,15 @@ use http_service::Body; -use crate::body; - -/// An HTTP response. -/// -/// A convenient alias for the `http::Response` type, using Tide's `Body`. -pub type Response = http::Response; +pub type Response = http_service::Response; + +pub fn json(t: T) -> Response { + // TODO: remove the `unwrap` + http::Response::builder() + .status(http::status::StatusCode::OK) + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_vec(&t).unwrap())) + .unwrap() +} /// A value that is synchronously convertable into a `Response`. pub trait IntoResponse: Send + Sized { @@ -46,12 +50,6 @@ impl IntoResponse for Vec { } } -impl IntoResponse for body::Bytes { - fn into_response(self) -> Response { - self.to_vec().into_response() - } -} - impl IntoResponse for String { fn into_response(self) -> Response { http::Response::builder() diff --git a/src/route.rs b/src/route.rs new file mode 100644 index 000000000..d3309efe7 --- /dev/null +++ b/src/route.rs @@ -0,0 +1,80 @@ +use crate::{router::Router, Endpoint}; + +/// A handle to a resource (identified by a path). +/// +/// All HTTP requests are made against resources. After using `App::at` (or `App::at`) to +/// establish a resource path, the `Resource` type can be used to establish endpoints for various +/// HTTP methods at that path. Also, using `nest`, it can be used to set up a subrouter. +/// +/// After establishing an endpoint, the method will return `&mut EndpointData`. This can be used to +/// set per-endpoint configuration. +pub struct Route<'a, AppData> { + router: &'a mut Router, + path: String, +} + +impl<'a, AppData> Route<'a, AppData> { + pub(crate) fn new(router: &'a mut Router, path: String) -> Route<'a, AppData> { + Route { router, path } + } + + /// Add an endpoint for the given HTTP method + pub fn method(&mut self, method: http::Method, ep: impl Endpoint) -> &mut Self { + self.router.add(&self.path, method, ep); + self + } + + /// Add an endpoint for `GET` requests + pub fn get(&mut self, ep: impl Endpoint) -> &mut Self { + self.method(http::Method::GET, ep); + self + } + + /// Add an endpoint for `HEAD` requests + pub fn head(&mut self, ep: impl Endpoint) -> &mut Self { + self.method(http::Method::HEAD, ep); + self + } + + /// Add an endpoint for `PUT` requests + pub fn put(&mut self, ep: impl Endpoint) -> &mut Self { + self.method(http::Method::PUT, ep); + self + } + + /// Add an endpoint for `POST` requests + pub fn post(&mut self, ep: impl Endpoint) -> &mut Self { + self.method(http::Method::POST, ep); + self + } + + /// Add an endpoint for `DELETE` requests + pub fn delete(&mut self, ep: impl Endpoint) -> &mut Self { + self.method(http::Method::DELETE, ep); + self + } + + /// Add an endpoint for `OPTIONS` requests + pub fn options(&mut self, ep: impl Endpoint) -> &mut Self { + self.method(http::Method::OPTIONS, ep); + self + } + + /// Add an endpoint for `CONNECT` requests + pub fn connect(&mut self, ep: impl Endpoint) -> &mut Self { + self.method(http::Method::CONNECT, ep); + self + } + + /// Add an endpoint for `PATCH` requests + pub fn patch(&mut self, ep: impl Endpoint) -> &mut Self { + self.method(http::Method::PATCH, ep); + self + } + + /// Add an endpoint for `TRACE` requests + pub fn trace(&mut self, ep: impl Endpoint) -> &mut Self { + self.method(http::Method::TRACE, ep); + self + } +} diff --git a/src/router.rs b/src/router.rs index ef542203c..1c2aecb60 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,633 +1,57 @@ -use std::any::Any; -use std::collections::HashMap; -use std::fmt::Debug; -use std::sync::Arc; +use fnv::FnvHashMap; +use futures::future::FutureObj; +use route_recognizer::{Match, Params, Router as MethodRouter}; -use crate::{ - configuration::Store, - endpoint::{BoxedEndpoint, Endpoint}, - Middleware, -}; -use path_table::{PathTable, RouteMatch}; +use crate::endpoint::{DynEndpoint, Endpoint}; -/// A core type for routing. +/// The routing table used by `App` /// -/// The `Router` type can be used to set up routes and resources, and to apply middleware. -pub struct Router { - table: PathTable>, - middleware_base: Vec + Send + Sync>>, - pub(crate) store_base: Store, +/// Internally, we have a separate state machine per http method; indexing +/// by the method first allows the table itself to be more efficient. +pub(crate) struct Router { + method_map: FnvHashMap>>>, } -pub(crate) struct RouteResult<'a, Data> { - pub(crate) endpoint: &'a EndpointData, - pub(crate) params: Option>, - pub(crate) middleware: &'a [Arc + Send + Sync>], +/// The result of routing a URL +pub(crate) struct RouteResult<'a, AppData> { + pub(crate) endpoint: &'a DynEndpoint, + pub(crate) params: Params, } -fn route_match_success<'a, Data>( - route: &'a ResourceData, - route_match: RouteMatch<'a>, - method: &http::Method, -) -> Option> { - // If it is a HTTP HEAD request then check if there is a callback in the endpoints map - // if not then fallback to the behavior of HTTP GET else proceed as usual - let endpoint = - if method == http::Method::HEAD && !route.endpoints.contains_key(&http::Method::HEAD) { - route.endpoints.get(&http::Method::GET)? - } else { - route.endpoints.get(method)? - }; - let middleware = &*route.middleware; - - Some(RouteResult { - endpoint, - params: Some(route_match), - middleware, - }) -} - -fn route_match_failure<'a, Data>( - endpoint: &'a EndpointData, - middleware: &'a [Arc + Send + Sync>], -) -> RouteResult<'a, Data> { - RouteResult { - endpoint, - params: None, - middleware: &*middleware, - } -} - -impl Router { - /// Add a new resource at the given `path`, relative to this router. - /// - /// Routing means mapping an HTTP request to an endpoint. Here Tide applies a "table of - /// contents" approach, which makes it easy to see the overall app structure. Endpoints are - /// selected solely by the path and HTTP method of a request: the path determines the resource - /// and the HTTP verb the respective endpoint of the selected resource. Example: - /// - /// ```rust,no_run - /// # #![feature(async_await)] - /// # let mut app = tide::App::new(()); - /// app.at("/").get(async || "Hello, world!"); - /// ``` - /// - /// A path is comprised of zero or many segments, i.e. non-empty strings separated by '/'. There - /// are two kinds of segments: concrete and wildcard. A concrete segment is used to exactly - /// match the respective part of the path of the incoming request. A wildcard segment on the - /// other hand extracts and parses the respective part of the path of the incoming request to - /// pass it along to the endpoint as an argument. A wildcard segment is either defined by "{}" - /// or by "{name}" for a so called named wildcard segment which can be extracted using - /// `NamedSegment`. It is not possible to define wildcard segments with different names for - /// otherwise identical paths. - /// - /// Wildcard definitions can be followed by an optional *wildcard modifier*. Currently, there is - /// only one modifier: `*`, which means that the wildcard will match to the end of given path, - /// no matter how many segments are left, even nothing. If there is a modifier for unnamed - /// wildcard definition, `{}` may be omitted. That is, `{}*` can be written as `*`. It is an - /// error to define two wildcard segments with different wildcard modifiers, or to write other - /// path segment after a segment with wildcard modifier. - /// - /// Here are some examples omitting the HTTP verb based endpoint selection: - /// - /// ```rust,no_run - /// # let mut app = tide::App::new(()); - /// app.at("/"); - /// app.at("/hello"); - /// app.at("/message/{}"); - /// app.at("add_two/{num}"); - /// app.at("static/{path}*"); - /// app.at("single_page_app/*"); - /// ``` - /// - /// Notice that there is no fallback route matching, i.e. either a resource is a full match or - /// not, which means that the order of adding resources has no effect. - pub fn at<'a>(&'a mut self, path: &'a str) -> Resource<'a, Data> { - let table = self.table.setup_table(path); - Resource { - table, - middleware_base: &self.middleware_base, - } - } - - /// Create a new top-level router. - pub(crate) fn new() -> Router { +impl Router { + pub(crate) fn new() -> Router { Router { - table: PathTable::new(), - middleware_base: Vec::new(), - store_base: Store::new(), - } - } - - /// Apply `middleware` to this router. - /// - /// Note that the order of nesting subrouters and applying middleware matters. If there are - /// nested subrouters *before* the method call, the given middleware will be applied *after* - /// the subrouter middleware. - /// - /// ``` - /// # #![feature(futures_api, async_await)] - /// # fn passthrough_middleware( - /// # ctx: tide::middleware::RequestContext, - /// # ) -> futures::future::FutureObj { - /// # ctx.next() - /// # } - /// # let mut app = tide::App::new(()); - /// # let router = app.router(); - /// router.at("a1").nest(|router| { - /// # let a = passthrough_middleware; - /// router.middleware(a); - /// router.at("").get(async || "A then B"); - /// }); - /// # let b = passthrough_middleware; - /// router.middleware(b); - /// router.at("a2").nest(|router| { - /// # let a = passthrough_middleware; - /// router.middleware(a); - /// router.at("").get(async || "B then A"); - /// }); - /// ``` - pub fn middleware(&mut self, middleware: impl Middleware + 'static) -> &mut Self { - let middleware = Arc::new(middleware); - for resource in self.table.iter_mut() { - resource.middleware.push(middleware.clone()); - } - self.middleware_base.push(middleware); - self - } - - /// Add a default configuration `item` for this router. - /// - /// The default configuration will be applied when the router setup ends. - pub fn config(&mut self, item: T) -> &mut Self { - self.store_base.write(item); - self - } - - pub(crate) fn route<'a>( - &'a self, - path: &'a str, - method: &http::Method, - default_handler: &'a Arc>, - ) -> RouteResult<'a, Data> { - match self.table.route(path) { - Some((route, route_match)) => route_match_success(route, route_match, method) - .unwrap_or_else(|| route_match_failure(default_handler, &self.middleware_base)), - None => route_match_failure(default_handler, &self.middleware_base), - } - } -} - -impl Router { - pub(crate) fn apply_default_config(&mut self) { - for resource in self.table.iter_mut() { - for endpoint in resource.endpoints.values_mut() { - endpoint.store.merge(&self.store_base); - } - } - } - - pub(crate) fn get_item(&self) -> Option<&T> { - self.store_base.read() - } -} - -/// A handle to the endpoint. -/// -/// This can be used to add configuration items to the endpoint. -pub struct EndpointData { - pub(crate) endpoint: BoxedEndpoint, - pub(crate) store: Store, -} - -impl EndpointData { - /// Add a configuration `item` for this endpoint. - pub fn config(&mut self, item: T) -> &mut Self { - self.store.write(item); - self - } -} - -/// A handle to a resource (identified by a path). -/// -/// All HTTP requests are made against resources. After using `Router::at` (or `App::at`) to -/// establish a resource path, the `Resource` type can be used to establish endpoints for various -/// HTTP methods at that path. Also, using `nest`, it can be used to set up a subrouter. -/// -/// After establishing an endpoint, the method will return `&mut EndpointData`. This can be used to -/// set per-endpoint configuration. -pub struct Resource<'a, Data> { - table: &'a mut PathTable>, - middleware_base: &'a Vec + Send + Sync>>, -} - -struct ResourceData { - endpoints: HashMap>, - middleware: Vec + Send + Sync>>, -} - -impl<'a, Data> Resource<'a, Data> { - /// "Nest" a subrouter to the path. - /// - /// This method will build a fresh `Router` and give a mutable reference to it to the builder - /// function. Builder can set up a subrouter using the `Router`. All middleware applied inside - /// the builder will be local to the subrouter and its descendents. - /// - /// If resources are already present, they will be discarded. - pub fn nest(self, builder: impl FnOnce(&mut Router)) { - let mut subrouter = Router { - table: PathTable::new(), - middleware_base: self.middleware_base.clone(), - store_base: Store::new(), - }; - builder(&mut subrouter); - subrouter.apply_default_config(); - *self.table = subrouter.table; - } - - /// Add an endpoint for the given HTTP method - pub fn method, U>( - &mut self, - method: http::Method, - ep: T, - ) -> &mut EndpointData { - let resource = self.table.resource_mut(); - if resource.is_none() { - let new_resource = ResourceData { - endpoints: HashMap::new(), - middleware: self.middleware_base.clone(), - }; - *resource = Some(new_resource); - } - let resource = resource.as_mut().unwrap(); - - let entry = resource.endpoints.entry(method); - if let std::collections::hash_map::Entry::Occupied(ep) = entry { - panic!("A {} endpoint already exists for this path", ep.key()) - } - - let endpoint = EndpointData { - endpoint: BoxedEndpoint::new(ep), - store: Store::new(), - }; - - entry.or_insert(endpoint) - } - - /// Add an endpoint for `GET` requests - pub fn get, U>(&mut self, ep: T) -> &mut EndpointData { - self.method(http::Method::GET, ep) - } - - /// Add an endpoint for `HEAD` requests - pub fn head, U>(&mut self, ep: T) -> &mut EndpointData { - self.method(http::Method::HEAD, ep) - } - - /// Add an endpoint for `PUT` requests - pub fn put, U>(&mut self, ep: T) -> &mut EndpointData { - self.method(http::Method::PUT, ep) - } - - /// Add an endpoint for `POST` requests - pub fn post, U>(&mut self, ep: T) -> &mut EndpointData { - self.method(http::Method::POST, ep) - } - - /// Add an endpoint for `DELETE` requests - pub fn delete, U>(&mut self, ep: T) -> &mut EndpointData { - self.method(http::Method::DELETE, ep) - } - - /// Add an endpoint for `OPTIONS` requests - pub fn options, U>(&mut self, ep: T) -> &mut EndpointData { - self.method(http::Method::OPTIONS, ep) - } - - /// Add an endpoint for `CONNECT` requests - pub fn connect, U>(&mut self, ep: T) -> &mut EndpointData { - self.method(http::Method::CONNECT, ep) - } - - /// Add an endpoint for `PATCH` requests - pub fn patch, U>(&mut self, ep: T) -> &mut EndpointData { - self.method(http::Method::PATCH, ep) - } - - /// Add an endpoint for `TRACE` requests - pub fn trace, U>(&mut self, ep: T) -> &mut EndpointData { - self.method(http::Method::TRACE, ep) - } -} - -#[cfg(test)] -mod tests { - use futures::{executor::block_on, future::FutureObj}; - - use super::*; - use crate::{middleware::RequestContext, AppData, Response}; - - fn passthrough_middleware( - ctx: RequestContext, - ) -> FutureObj { - ctx.next() - } - - async fn simulate_request<'a, Data: Default + Clone + Send + Sync + 'static>( - router: &'a Router, - path: &'a str, - method: &'a http::Method, - ) -> Option { - let default_handler = Arc::new(EndpointData { - endpoint: BoxedEndpoint::new(async || http::status::StatusCode::NOT_FOUND), - store: Store::new(), - }); - let RouteResult { - endpoint, - params, - middleware, - } = router.route(path, method, &default_handler); - - let data = Data::default(); - let req = http::Request::builder() - .method(method) - .body(http_service::Body::empty()) - .unwrap(); - - let ctx = RequestContext { - app_data: data, - req, - params, - endpoint, - next_middleware: middleware, - }; - let res = await!(ctx.next()); - Some(res.map(Into::into)) - } - - fn route_middleware_count( - router: &Router, - path: &str, - method: &http::Method, - ) -> Option { - let default_handler = Arc::new(EndpointData { - endpoint: BoxedEndpoint::new(async || http::status::StatusCode::NOT_FOUND), - store: Store::new(), - }); - let route_result = router.route(path, method, &default_handler); - Some(route_result.middleware.len()) - } - - #[test] - fn simple_static() { - let mut router: Router<()> = Router::new(); - router.at("/").get(async || "/"); - router.at("/foo").get(async || "/foo"); - router.at("/foo/bar").get(async || "/foo/bar"); - - for path in &["/", "/foo", "/foo/bar"] { - let res = - if let Some(res) = block_on(simulate_request(&router, path, &http::Method::GET)) { - res - } else { - panic!("Routing of path `{}` failed", path); - }; - let body = block_on(res.into_body().into_vec()).expect("Reading body should succeed"); - assert_eq!(&*body, path.as_bytes()); - } - } - - #[test] - fn nested_static() { - let mut router: Router<()> = Router::new(); - router.at("/a").get(async || "/a"); - router.at("/b").nest(|router| { - router.at("/").get(async || "/b"); - router.at("/a").get(async || "/b/a"); - router.at("/b").get(async || "/b/b"); - router.at("/c").nest(|router| { - router.at("/a").get(async || "/b/c/a"); - router.at("/b").get(async || "/b/c/b"); - }); - router.at("/d").get(async || "/b/d"); - }); - router.at("/a/a").nest(|router| { - router.at("/a").get(async || "/a/a/a"); - router.at("/b").get(async || "/a/a/b"); - }); - router.at("/a/b").nest(|router| { - router.at("/").get(async || "/a/b"); - }); - - for failing_path in &["/", "/a/a", "/a/b/a"] { - if let Some(res) = block_on(simulate_request(&router, failing_path, &http::Method::GET)) - { - if !res.status().is_client_error() { - panic!( - "Should have returned a client error when router cannot match with path {}", - failing_path - ); - } - } else { - panic!("Should have received a response from {}", failing_path); - }; - } - - for path in &[ - "/a", "/a/a/a", "/a/a/b", "/a/b", "/b", "/b/a", "/b/b", "/b/c/a", "/b/c/b", "/b/d", - ] { - let res = - if let Some(res) = block_on(simulate_request(&router, path, &http::Method::GET)) { - res - } else { - panic!("Routing of path `{}` failed", path); - }; - let body = block_on(res.into_body().into_vec()).expect("Reading body should succeed"); - assert_eq!(&*body, path.as_bytes()); - } - } - - #[test] - fn multiple_methods() { - let mut router: Router<()> = Router::new(); - router.at("/a").nest(|router| { - router.at("/b").get(async || "/a/b GET"); - }); - router.at("/a/b").post(async || "/a/b POST"); - - for (path, method) in &[("/a/b", http::Method::GET), ("/a/b", http::Method::POST)] { - let res = if let Some(res) = block_on(simulate_request(&router, path, &method)) { - res - } else { - panic!("Routing of {} `{}` failed", method, path); - }; - let body = block_on(res.into_body().into_vec()).expect("Reading body should succeed"); - assert_eq!(&*body, format!("{} {}", path, method).as_bytes()); + method_map: FnvHashMap::default(), } } - #[test] - #[should_panic] - fn duplicate_endpoint_fails() { - let mut router: Router<()> = Router::new(); - router.at("/a").nest(|router| { - router.at("/b").get(async || ""); - }); // flattened into /a/b - router.at("/a/b").get(async || "duplicate"); - } - - #[test] - fn simple_middleware() { - let mut router: Router<()> = Router::new(); - router.middleware(passthrough_middleware); - router.at("/").get(async || "/"); - router.at("/b").nest(|router| { - router.at("/").get(async || "/b"); - router.middleware(passthrough_middleware); - }); - - assert_eq!( - route_middleware_count(&router, "/", &http::Method::GET), - Some(1) - ); - assert_eq!( - route_middleware_count(&router, "/b", &http::Method::GET), - Some(2) - ); + pub(crate) fn add(&mut self, path: &str, method: http::Method, ep: impl Endpoint) { + self.method_map + .entry(method) + .or_insert_with(MethodRouter::new) + .add( + path, + Box::new(move |cx| FutureObj::new(Box::new(ep.call(cx)))), + ) } - #[test] - fn middleware_apply_order() { - #[derive(Default, Clone, Debug)] - struct Data(Vec); - struct Pusher(usize); - impl Middleware for Pusher { - fn handle<'a>(&'a self, mut ctx: RequestContext<'a, Data>) -> FutureObj<'a, Response> { - FutureObj::new(Box::new( - async move { - ctx.app_data.0.push(self.0); - await!(ctx.next()) - }, - )) + pub(crate) fn route(&self, path: &str, method: http::Method) -> RouteResult<'_, AppData> { + if let Some(Match { handler, params }) = self + .method_map + .get(&method) + .and_then(|r| r.recognize(path).ok()) + { + RouteResult { + endpoint: &**handler, + params, } - } - - // The order of endpoint and middleware does not matter - // The order of subrouter and middleware DOES matter - let mut router: Router = Router::new(); - router.middleware(Pusher(0)); - router.at("/").get(async move |data: AppData| { - if (data.0).0 == [0, 2] { - http::StatusCode::OK - } else { - http::StatusCode::INTERNAL_SERVER_ERROR - } - }); - router.at("/a").nest(|router| { - router.at("/").get(async move |data: AppData| { - if (data.0).0 == [0, 1, 2] { - http::StatusCode::OK - } else { - http::StatusCode::INTERNAL_SERVER_ERROR - } - }); - router.middleware(Pusher(1)); - }); - router.middleware(Pusher(2)); - router.at("/b").nest(|router| { - router.at("/").get(async move |data: AppData| { - if (data.0).0 == [0, 2, 1] { - http::StatusCode::OK - } else { - http::StatusCode::INTERNAL_SERVER_ERROR - } - }); - router.middleware(Pusher(1)); - }); - - for path in &["/", "/a", "/b"] { - let res = block_on(simulate_request(&router, path, &http::Method::GET)).unwrap(); - assert_eq!(res.status(), 200); - } - } - - #[test] - fn configuration() { - use crate::ExtractConfiguration; - async fn endpoint( - ExtractConfiguration(x): ExtractConfiguration<&'static str>, - ) -> &'static str { - x.unwrap() - } - - let mut router: Router<()> = Router::new(); - router.config("foo"); - router.at("/").get(endpoint); - router.at("/bar").get(endpoint).config("bar"); - router.apply_default_config(); // simulating App behavior + } else if method == http::Method::HEAD { + // If it is a HTTP HEAD request then check if there is a callback in the endpoints map + // if not then fallback to the behavior of HTTP GET else proceed as usual - let res = block_on(simulate_request(&router, "/", &http::Method::GET)).unwrap(); - let body = block_on(res.into_body().into_vec()).unwrap(); - assert_eq!(&*body, &*b"foo"); - - let res = block_on(simulate_request(&router, "/bar", &http::Method::GET)).unwrap(); - let body = block_on(res.into_body().into_vec()).unwrap(); - assert_eq!(&*body, &*b"bar"); - } - - #[test] - fn configuration_nested() { - use crate::ExtractConfiguration; - async fn endpoint( - ExtractConfiguration(x): ExtractConfiguration<&'static str>, - ) -> &'static str { - x.unwrap() - } - - let mut router: Router<()> = Router::new(); - router.config("foo"); - router.at("/").get(endpoint); - router.at("/bar").nest(|router| { - router.config("bar"); - router.at("/").get(endpoint); - router.at("/baz").get(endpoint).config("baz"); - }); - router.apply_default_config(); // simulating App behavior - - let res = block_on(simulate_request(&router, "/", &http::Method::GET)).unwrap(); - let body = block_on(res.into_body().into_vec()).unwrap(); - assert_eq!(&*body, &*b"foo"); - - let res = block_on(simulate_request(&router, "/bar", &http::Method::GET)).unwrap(); - let body = block_on(res.into_body().into_vec()).unwrap(); - assert_eq!(&*body, &*b"bar"); - - let res = block_on(simulate_request(&router, "/bar/baz", &http::Method::GET)).unwrap(); - let body = block_on(res.into_body().into_vec()).unwrap(); - assert_eq!(&*body, &*b"baz"); - } - - #[test] - fn configuration_order() { - use crate::ExtractConfiguration; - async fn endpoint( - ExtractConfiguration(x): ExtractConfiguration<&'static str>, - ) -> &'static str { - x.unwrap() + self.route(path, http::Method::GET) + } else { + panic!() } - - let mut router: Router<()> = Router::new(); - router.at("/").get(endpoint); - router.config("foo"); // order does not matter - router.at("/bar").get(endpoint).config("bar"); - router.apply_default_config(); // simulating App behavior - - let res = block_on(simulate_request(&router, "/", &http::Method::GET)).unwrap(); - let body = block_on(res.into_body().into_vec()).unwrap(); - assert_eq!(&*body, &*b"foo"); - - let res = block_on(simulate_request(&router, "/bar", &http::Method::GET)).unwrap(); - let body = block_on(res.into_body().into_vec()).unwrap(); - assert_eq!(&*body, &*b"bar"); } } diff --git a/src/serve.rs b/src/serve.rs deleted file mode 100644 index bde0b2e20..000000000 --- a/src/serve.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Use hyper to serve the given HttpService at the given address -pub(crate) fn serve(s: S, addr: std::net::SocketAddr) { - http_service_hyper::serve(s, addr); -}