diff --git a/Cargo.toml b/Cargo.toml index 46d40d911..4e4ca3841 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" @@ -46,3 +47,5 @@ basic-cookies = "0.1.3" juniper = "0.10.0" structopt = "0.2.14" http-service-mock = "0.1.0" +serde = "1.0.80" +serde_derive = "1.0.80" diff --git a/examples/body_types.rs b/examples/body_types.rs old mode 100644 new mode 100755 index 2a5c4a018..a14c1c73b --- a/examples/body_types.rs +++ b/examples/body_types.rs @@ -1,8 +1,13 @@ -#![feature(async_await, futures_api)] +#![feature(async_await, futures_api, await_macro)] #[macro_use] extern crate serde_derive; -use tide::body; + +use tide::{ + error::ResultExt, + forms::{self, ExtractForms}, + response, App, Context, EndpointResult, +}; #[derive(Serialize, Deserialize, Clone, Debug)] struct Message { @@ -10,47 +15,37 @@ struct Message { contents: String, } -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_vec(msg: body::Bytes) -> Vec { - println!("Vec: {:?}", *msg); - msg.to_vec() -} - -async fn echo_bytes(msg: body::Bytes) -> body::Bytes { - println!("Bytes: {:?}", *msg); +async fn echo_string(mut cx: Context<()>) -> String { + let msg = await!(cx.body_string()).unwrap(); + println!("String: {}", msg); msg } -async fn echo_json(msg: body::Json) -> body::Json { - println!("JSON: {:?}", *msg); - +async fn echo_bytes(mut cx: Context<()>) -> Vec { + let msg = await!(cx.body_bytes()).unwrap(); + println!("Bytes: {:?}", msg); msg } -async fn echo_form(msg: body::Form) -> body::Form { - println!("Form: {:?}", *msg); +async fn echo_json(mut cx: Context<()>) -> EndpointResult { + let msg = await!(cx.body_json()).client_err()?; + println!("JSON: {:?}", msg); + Ok(response::json(msg)) +} - msg +async fn echo_form(mut cx: Context<()>) -> EndpointResult { + let msg = await!(cx.body_form())?; + println!("Form: {:?}", msg); + Ok(forms::form(msg)) } fn main() { - let mut app = tide::App::new(()); + let mut app = App::new(()); app.at("/echo/string").post(echo_string); - app.at("/echo/string_lossy").post(echo_string_lossy); - app.at("/echo/vec").post(echo_vec); app.at("/echo/bytes").post(echo_bytes); app.at("/echo/json").post(echo_json); app.at("/echo/form").post(echo_form); - app.serve(); + app.serve("127.0.0.1:8000").unwrap(); } diff --git a/examples/catch_all.rs b/examples/catch_all.rs old mode 100644 new mode 100755 index 0dd7bb360..355262343 --- a/examples/catch_all.rs +++ b/examples/catch_all.rs @@ -1,14 +1,14 @@ #![feature(async_await, futures_api)] -async fn echo_path(path: tide::head::Path) -> String { - format!("Your path is: {}", *path) +use tide::Context; + +async fn echo_path(cx: Context<()>) -> String { + let path: String = cx.param("path").unwrap(); + format!("Your path is: {}", path) } fn main() { let mut app = tide::App::new(()); - app.at("/echo_path").nest(|router| { - router.at("*").get(echo_path); - }); - - app.serve(); + app.at("/echo_path/:path*").get(echo_path); + app.serve("127.0.0.1:8000").unwrap(); } diff --git a/examples/cli_parsing.rs b/examples/cli_parsing.rs deleted file mode 100644 index 421dc5d3a..000000000 --- a/examples/cli_parsing.rs +++ /dev/null @@ -1,48 +0,0 @@ -#![feature(async_await, futures_api)] - -use structopt::StructOpt; -use tide::{configuration::Configuration, ExtractConfiguration}; - -#[derive(Debug, StructOpt)] -#[structopt(name = "tide_example")] -struct Settings { - #[structopt( - short = "a", - long = "address", - env = "TIDE_ADDRESS", - default_value = "127.0.0.1" - )] - address: String, - #[structopt(short = "p", long = "port", env = "TIDE_PORT", default_value = "8000")] - port: u16, - #[structopt( - short = "d", - long = "database", - env = "TIDE_DATABASE", - default_value = "none" - )] - database: String, -} - -async fn reply( - // `ExtractConfiguration` will extract the configuration item of given type, and provide it as - // `Option`. If it is not set, the inner value will be `None`. - ExtractConfiguration(config): ExtractConfiguration -) -> String { - let config = config.unwrap(); - format!("We are running in {:?}", config.env) -} - -fn main() { - let settings = Settings::from_args(); - let app_config = Configuration::build() - .address(settings.address) - .port(settings.port) - .finalize(); - - let mut app = tide::App::new(()); - app.config(app_config); - - app.at("/").get(reply); - app.serve(); -} diff --git a/examples/computed_values.rs b/examples/computed_values.rs deleted file mode 100644 index 50bf8504b..000000000 --- a/examples/computed_values.rs +++ /dev/null @@ -1,38 +0,0 @@ -#![feature(async_await, futures_api)] -extern crate basic_cookies; - -use basic_cookies::Cookie; -use std::collections::HashMap; -use tide::{Compute, Computed, Request}; - -#[derive(Clone, Debug)] -struct Cookies { - content: HashMap, -} - -impl Compute for Cookies { - fn compute_fresh(req: &mut Request) -> Self { - let content = if let Some(raw_cookies) = req.headers().get("Cookie") { - Cookie::parse(raw_cookies.to_str().unwrap()) - .unwrap() - .iter() - .map(|c| (c.get_name().into(), c.get_value().into())) - .collect() - } else { - HashMap::new() - }; - - Cookies { content } - } -} - -async fn hello_cookies(Computed(cookies): Computed) -> String { - format!("hello cookies: {:?}", cookies) -} - -fn main() { - let mut app = tide::App::new(()); - app.at("/").get(hello_cookies); - - app.serve(); -} diff --git a/examples/configuration.rs b/examples/configuration.rs deleted file mode 100644 index f376ae06e..000000000 --- a/examples/configuration.rs +++ /dev/null @@ -1,34 +0,0 @@ -#![feature(async_await, futures_api)] - -use futures::future::FutureObj; -use tide::{head::Path, middleware::RequestContext, ExtractConfiguration, Response}; - -/// A type that represents how much value will be added by the `add` handler. -#[derive(Clone, Debug, Default)] -struct IncreaseBy(i32); - -async fn add( - Path(base): Path, - // `ExtractConfiguration` will extract the configuration item of given type, and provide it as - // `Option`. If it is not set, the inner value will be `None`. - ExtractConfiguration(amount): ExtractConfiguration, -) -> String { - let IncreaseBy(amount) = amount.unwrap_or_default(); - format!("{} plus {} is {}", base, amount, base + amount) -} - -fn debug_store(ctx: RequestContext<()>) -> FutureObj { - println!("{:#?}", ctx.store()); - ctx.next() -} - -fn main() { - let mut app = tide::App::new(()); - // `App::config` sets the default configuration of the app (that is, a top-level router). - app.config(IncreaseBy(1)); - app.middleware(debug_store); - app.at("add_one/{}").get(add); // `IncreaseBy` is set to 1 - app.at("add_two/{}").get(add).config(IncreaseBy(2)); // `IncreaseBy` is overridden to 2 - - app.serve(); -} diff --git a/examples/cookie_extractor.rs b/examples/cookie_extractor.rs old mode 100644 new mode 100755 index f081f72b5..eb31b42cd --- a/examples/cookie_extractor.rs +++ b/examples/cookie_extractor.rs @@ -1,18 +1,15 @@ #![feature(async_await, futures_api)] -use tide::Cookies; +use tide::{cookies::ExtractCookies, Context}; /// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter. /// -async fn hello_cookies(cookies: Cookies) -> String { - format!("hello cookies: {:?}", cookies) +async fn hello_cookies(mut cx: Context<()>) -> String { + format!("hello cookies: {:?}", cx.cookie("hello")) } fn main() { let mut app = tide::App::new(()); app.at("/").get(hello_cookies); - - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(); + app.serve("127.0.0.1:8000").unwrap(); } diff --git a/examples/default_handler.rs b/examples/default_handler.rs deleted file mode 100644 index 6472b0a15..000000000 --- a/examples/default_handler.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![feature(async_await)] - -use http::status::StatusCode; -use tide::IntoResponse; - -fn main() { - let mut app = tide::App::new(()); - app.at("/").get(async || "Hello, world!"); - - app.default_handler(async || "¯\\_(ツ)_/¯".with_status(StatusCode::NOT_FOUND)); - - app.serve() -} diff --git a/examples/default_headers.rs b/examples/default_headers.rs old mode 100644 new mode 100755 index 01eb5d6dd..4d8890adf --- a/examples/default_headers.rs +++ b/examples/default_headers.rs @@ -11,7 +11,7 @@ fn main() { .header("X-Server", "Tide"), ); - app.at("/").get(async || "Hello, world!"); + app.at("/").get(async move |_| "Hello, world!"); - app.serve(); + app.serve("127.0.0.1:8000").unwrap(); } diff --git a/examples/graphql.rs b/examples/graphql.rs index c976353f5..5d3d834d6 100644 --- a/examples/graphql.rs +++ b/examples/graphql.rs @@ -3,26 +3,26 @@ // // [the Juniper book]: https://graphql-rust.github.io/ -#![feature(async_await, futures_api)] +#![feature(async_await, futures_api, await_macro)] use http::status::StatusCode; use juniper::graphql_object; use std::sync::{atomic, Arc}; -use tide::{body, App, AppData, IntoResponse, Response}; +use tide::{error::ResultExt, response, App, Context, EndpointResult}; -// First, we define `Context` that holds accumulator state. This is accessible as App data in +// First, we define `Data` that holds accumulator state. This is accessible as App data in // Tide, and as executor context in Juniper. #[derive(Clone, Default)] -struct Context(Arc); +struct Data(Arc); -impl juniper::Context for Context {} +impl juniper::Context for Data {} // We define `Query` unit struct here. GraphQL queries will refer to this struct. The struct itself // doesn't have any associated data (and there's no need to do so), but instead it exposes the // accumulator state from the context. struct Query; -graphql_object!(Query: Context |&self| { +graphql_object!(Query: Data |&self| { // GraphQL integers are signed and 32 bits long. field accumulator(&executor) -> i32 as "Current value of the accumulator" { executor.context().0.load(atomic::Ordering::Relaxed) as i32 @@ -33,7 +33,7 @@ graphql_object!(Query: Context |&self| { // `Query`, but it provides the way to "mutate" the accumulator state. struct Mutation; -graphql_object!(Mutation: Context |&self| { +graphql_object!(Mutation: Data |&self| { field add(&executor, by: i32) -> i32 as "Add given value to the accumulator." { executor.context().0.fetch_add(by as isize, atomic::Ordering::Relaxed) as i32 + by } @@ -45,23 +45,21 @@ type Schema = juniper::RootNode<'static, Query, Mutation>; // Finally, we'll bridge between Tide and Juniper. `GraphQLRequest` from Juniper implements // `Deserialize`, so we use `Json` extractor to deserialize the request body. -async fn handle_graphql( - ctx: AppData, - query: body::Json, -) -> Response { - let response = query.execute(&Schema::new(Query, Mutation), &ctx); +async fn handle_graphql(mut cx: Context) -> EndpointResult { + let query: juniper::http::GraphQLRequest = await!(cx.body_json()).client_err()?; + let response = query.execute(&Schema::new(Query, Mutation), cx.app_data()); let status = if response.is_ok() { StatusCode::OK } else { StatusCode::BAD_REQUEST }; - body::Json(response).with_status(status).into_response() + let mut resp = response::json(response); + *resp.status_mut() = status; + Ok(resp) } fn main() { - let mut app = App::new(Context::default()); - + let mut app = App::new(Data::default()); app.at("/graphql").post(handle_graphql); - - app.serve(); + app.serve("127.0.0.1:8000").unwrap(); } diff --git a/examples/hello.rs b/examples/hello.rs old mode 100644 new mode 100755 index b3e113c8e..32bbddc12 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -2,7 +2,6 @@ fn main() { let mut app = tide::App::new(()); - app.at("/").get(async || "Hello, world!"); - - app.serve(); + app.at("/").get(async move |_| "Hello, world!"); + app.serve("127.0.0.1:8000").unwrap(); } diff --git a/examples/messages.rs b/examples/messages.rs old mode 100644 new mode 100755 index 92a6f5ef8..c1ea5e972 --- a/examples/messages.rs +++ b/examples/messages.rs @@ -1,15 +1,15 @@ -#![feature(async_await, futures_api)] +#![feature(async_await, futures_api, await_macro)] #[macro_use] extern crate serde_derive; use http::status::StatusCode; -use std::sync::{Arc, Mutex}; -use tide::{body, head, App, AppData}; +use std::sync::Mutex; +use tide::{error::ResultExt, response, App, Context, EndpointResult}; -#[derive(Clone, Default)] +#[derive(Default)] struct Database { - contents: Arc>>, + contents: Mutex>, } #[derive(Serialize, Deserialize, Clone)] @@ -19,17 +19,17 @@ struct Message { } impl Database { - fn insert(&mut self, msg: Message) -> usize { + fn insert(&self, msg: Message) -> usize { let mut table = self.contents.lock().unwrap(); table.push(msg); table.len() - 1 } - fn get(&mut self, id: usize) -> Option { + fn get(&self, id: usize) -> Option { self.contents.lock().unwrap().get(id).cloned() } - fn set(&mut self, id: usize, msg: Message) -> bool { + fn set(&self, id: usize, msg: Message) -> bool { let mut table = self.contents.lock().unwrap(); if let Some(old_msg) = table.get_mut(id) { @@ -41,39 +41,34 @@ impl Database { } } -async fn new_message(mut db: AppData, msg: body::Json) -> String { - db.insert(msg.clone()).to_string() +async fn new_message(mut cx: Context) -> EndpointResult { + let msg = await!(cx.body_json()).client_err()?; + Ok(cx.app_data().insert(msg).to_string()) } -async fn set_message( - mut db: AppData, - id: head::Path, - msg: body::Json, -) -> Result<(), StatusCode> { - if db.set(*id, msg.clone()) { +async fn set_message(mut cx: Context) -> EndpointResult<()> { + let msg = await!(cx.body_json()).client_err()?; + let id = cx.param("id").client_err()?; + + if cx.app_data().set(id, msg) { Ok(()) } else { - Err(StatusCode::NOT_FOUND) + Err(StatusCode::NOT_FOUND)? } } -async fn get_message( - mut db: AppData, - id: head::Path, -) -> Result, StatusCode> { - if let Some(msg) = db.get(*id) { - Ok(body::Json(msg)) +async fn get_message(cx: Context) -> EndpointResult { + let id = cx.param("id").client_err()?; + if let Some(msg) = cx.app_data().get(id) { + Ok(response::json(msg)) } else { - Err(StatusCode::NOT_FOUND) + Err(StatusCode::NOT_FOUND)? } } fn main() { let mut app = App::new(Database::default()); - app.at("/message").post(new_message); - app.at("/message/{}").get(get_message); - app.at("/message/{}").post(set_message); - - app.serve(); + app.at("/message/:id").get(get_message).post(set_message); + app.serve("127.0.0.1:8000").unwrap(); } diff --git a/examples/multipart-form/main.rs b/examples/multipart-form/main.rs old mode 100644 new mode 100755 index 070749582..7e083c177 --- a/examples/multipart-form/main.rs +++ b/examples/multipart-form/main.rs @@ -1,11 +1,10 @@ -#![feature(async_await, futures_api)] +#![feature(async_await, futures_api, await_macro)] #[macro_use] extern crate serde_derive; -use http::status::StatusCode; use std::io::Read; -use tide::{body, App}; +use tide::{forms::ExtractForms, response, App, Context, EndpointResult}; #[derive(Serialize, Deserialize, Clone)] struct Message { @@ -14,9 +13,7 @@ struct Message { file: Option, } -async fn upload_file( - mut multipart_form: body::MultipartForm, -) -> Result, StatusCode> { +async fn upload_file(mut cx: Context<()>) -> EndpointResult { // https://stackoverflow.com/questions/43424982/how-to-parse-multipart-forms-using-abonander-multipart-with-rocket let mut message = Message { key1: None, @@ -24,7 +21,7 @@ async fn upload_file( file: None, }; - multipart_form + await!(cx.body_multipart())? .foreach_entry(|mut entry| match entry.headers.name.as_str() { "file" => { let mut vec = Vec::new(); @@ -57,15 +54,13 @@ async fn upload_file( }) .expect("Unable to iterate multipart?"); - Ok(body::Json(message)) + Ok(response::json(message)) } fn main() { let mut app = App::new(()); - app.at("/upload_file").post(upload_file); - - app.serve(); + app.serve("127.0.0.1:8000").unwrap(); } // Test with: diff --git a/examples/named_path.rs b/examples/named_path.rs deleted file mode 100644 index 678cb6c10..000000000 --- a/examples/named_path.rs +++ /dev/null @@ -1,29 +0,0 @@ -#![feature(async_await, futures_api)] - -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 add_two(Named(number): Named) -> String { - let Number(num) = number; - format!("{} plus two is {}", num, num + 2) -} - -fn main() { - let mut app = tide::App::new(()); - app.at("add_two/{num}").get(add_two); - - app.serve(); -} diff --git a/examples/simple_nested_router.rs b/examples/simple_nested_router.rs deleted file mode 100644 index c7a9787b2..000000000 --- a/examples/simple_nested_router.rs +++ /dev/null @@ -1,38 +0,0 @@ -// A `Router`-nesting version of the example `named_path`. - -#![feature(async_await, futures_api)] - -use tide::{ - head::{Named, NamedSegment}, - Router, -}; - -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 add_two(Named(number): Named) -> String { - let Number(num) = number; - format!("{} plus two is {}", num, num + 2) -} - -fn build_add_two(router: &mut Router) { - router.at("{num}").get(add_two); -} - -fn main() { - let mut app = tide::App::new(()); - app.at("add_two").nest(build_add_two); - - app.serve(); -} diff --git a/src/app.rs b/src/app.rs index e86419539..1aeb0b2f7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,156 +1,218 @@ 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::{Router, Selection}, + Context, Route, }; -/// The top-level type for setting up a Tide application. +/// The entry point for building a Tide application. +/// +/// Apps are built up as a combination of *state*, *endpoints* and *middleware*: +/// +/// - Application state is user-defined, and is provided via the [`App:new`] +/// function. The state is available as a shared reference to all app endpoints. /// -/// Apps are equipped with a handle to their own state (`Data`), which is available to all endpoints. -/// This is a "handle" because it must be `Clone`, and endpoints are invoked with a fresh clone. -/// They also hold a top-level router. +/// - Endpoints provide the actual application-level code corresponding to +/// particular URLs. The [`App::at`] method creates a new *route* (using +/// standard router syntax), which can then be used to register endpoints +/// for particular HTTP request types. /// -/// # Examples +/// - Middleware extends the base Tide framework with additional request or +/// response processing, such as compression, default headers, or logging. To +/// add middleware to an app, use the [`App::middleware`] method. +/// +/// # Hello, world! /// /// You can start a simple Tide application that listens for `GET` requests at path `/hello` -/// on `127.0.0.1:8181` with: +/// on `127.0.0.1:8000` with: /// /// ```rust, no_run /// #![feature(async_await)] /// /// let mut app = tide::App::new(()); -/// app.at("/hello").get(async || "Hello, world!"); -/// app.serve() -/// ``` +/// app.at("/hello").get(async move |_| "Hello, world!"); +/// app.serve("127.0.0.1:8000"); +///``` +/// +/// # Routing and parameters /// -/// `App` state can be modeled with an underlying `Data` handle for a cloneable type `T`. Endpoints -/// can receive a fresh clone of that handle (in addition to data extracted from the request) by -/// defining a parameter of type `AppData`: +/// Tide's routing system is simple and similar to many other frameworks. It +/// uses `:foo` for "wildcard" URL segments, and `:foo*` to match the rest of a +/// URL (which may include multiple segments). Here's an example using wildcard +/// segments as parameters to endpoints: /// /// ```rust, no_run /// #![feature(async_await, futures_api)] /// -/// use std::sync::Arc; +/// use tide::error::ResultExt; +/// +/// async fn hello(cx: tide::Context<()>) -> tide::EndpointResult { +/// let user: String = cx.param("user").client_err()?; +/// Ok(format!("Hello, {}!", user)) +/// } +/// +/// async fn goodbye(cx: tide::Context<()>) -> tide::EndpointResult { +/// let user: String = cx.param("user").client_err()?; +/// Ok(format!("Goodbye, {}.", user)) +/// } +/// +/// let mut app = tide::App::new(()); +/// +/// app.at("/hello/:user").get(hello); +/// app.at("/goodbye/:user").get(goodbye); +/// app.at("/").get(async move |_| { +/// "Use /hello/{your name} or /goodbye/{your name}" +/// }); +/// +/// app.serve("127.0.0.1:8000"); +///``` +/// +/// You can learn more about routing in the [`App::at`] documentation. +/// +/// # Application state +/// +/// ```rust, no_run +/// #![feature(async_await, futures_api, await_macro)] +/// +/// use tide::{Context, EndpointResult, error::ResultExt, response, App}; +/// use http::StatusCode; /// use std::sync::Mutex; -/// use tide::AppData; -/// use tide::body; /// -/// #[derive(Clone, Default)] +/// #[derive(Default)] /// struct Database { -/// contents: Arc>>, +/// contents: Mutex>, /// } /// -/// async fn insert( -/// mut db: AppData, -/// msg: body::Str, -/// ) -> String { -/// // insert into db -/// # String::from("") +/// impl Database { +/// fn messages(&self) -> std::sync::MutexGuard> { +/// self.contents.lock().unwrap() +/// } /// } /// -/// fn main() { -/// let mut app = tide::App::new(Database::default()); -/// app.at("/messages/insert").post(insert); -/// app.serve() +/// #[derive(Serialize, Deserialize, Clone)] +/// struct Message { +/// author: Option, +/// contents: String, +/// } +/// +/// async fn new_message(cx: Context) -> EndpointResult { +/// let msg = await!(cx.body_json()).client_err()?; +/// +/// let mut messages = cx.app_data().messages(); +/// let id = messages.len(); +/// messages.push(msg); +/// +/// Ok(id.to_string()) /// } -/// ``` /// -/// Where to go from here: Please see [`Router`](struct.Router.html) and [`Endpoint`](trait.Endpoint.html) -/// for further examples. +/// async fn get_message(cx: Context) -> EndpointResult { +/// let id: usize = cx.param("id").client_err()?; +/// +/// if let Some(msg) = cx.app_data().messages().get(id) { +/// Ok(response::json(msg)) +/// } else { +/// Err(StatusCode::NOT_FOUND)? +/// } +/// } /// -pub struct App { - data: Data, - router: Router, - default_handler: EndpointData, +/// fn main() { +/// let mut app = App::new(Database::default()); +/// app.at("/message").post(new_message); +/// app.at("/message/:id").get(get_message); +/// app.serve("127.0.0.1:8000").unwrap(); +/// } + +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 + middleware: Vec::new(), + data, + } } - /// 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 new route at the given `path`, relative to root. + /// + /// 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 move |_| "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 written as `:name`, which creates an endpoint + /// parameter called `name`. 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. 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("add_two/:num"); + /// app.at("static/:path*"); + /// ``` + /// + /// 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()) } - /// Add a default configuration `item` for the whole app. - pub fn config(&mut self, item: T) -> &mut Self { - self.router.config(item); + /// Add middleware to an application. + /// + /// Middleware provides application-global customization of the + /// request/response cycle, such as compression, logging, or header + /// modification. Middleware is invoked when processing a request, and can + /// either continue processing (possibly modifying the response) or + /// immediately return a response. See the [`Middleware`] trait for details. + /// + /// Middleware can only be added at the "top level" of an application, + /// and is processed in the order in which it is applied. + pub fn middleware(&mut self, m: impl Middleware) -> &mut Self { + self.middleware.push(Arc::new(m)); self } - pub fn get_item(&self) -> Option<&T> { - self.router.get_item() - } - /// Make this app into an `HttpService`. - pub fn into_http_service(mut self) -> Server { - self.router.apply_default_config(); + /// + /// This lower-level method lets you host a Tide application within an HTTP + /// server of your choice, via the `http_service` interface crate. + 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 +220,30 @@ 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: impl std::net::ToSocketAddrs) -> std::io::Result<()> { + let addr = addr + .to_socket_addrs()? + .next() + .ok_or(std::io::ErrorKind::InvalidInput)?; println!("Server is listening on: http://{}", addr); - - crate::serve::serve(self.into_http_service(), addr); + http_service_hyper::serve(self.into_http_service(), addr); + Ok(()) } } +/// An instantiated Tide server. +/// +/// This type is useful only in conjunction with the [`HttpService`] trait, +/// i.e. for hosting a Tide app within some custom HTTP server. #[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 +253,127 @@ 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 Selection { 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; + use std::sync::Arc; -impl Deref for AppData { - type Target = T; - fn deref(&self) -> &T { - &self.0 + use super::*; + use crate::{middleware::Next, router::Selection, Context, Response}; + + fn simulate_request<'a, Data: Default + Clone + Send + Sync + 'static>( + app: &'a App, + path: &'a str, + method: http::Method, + ) -> FutureObj<'a, Response> { + let Selection { endpoint, params } = app.router.route(path, method.clone()); + + let data = Arc::new(Data::default()); + let req = http::Request::builder() + .method(method) + .body(http_service::Body::empty()) + .unwrap(); + let cx = Context::new(data, req, params); + let next = Next { + endpoint, + next_middleware: &app.middleware, + }; + + next.run(cx) } -} -impl DerefMut for AppData { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 + + #[test] + fn simple_static() { + let mut router = App::new(()); + router.at("/").get(async move |_| "/"); + router.at("/foo").get(async move |_| "/foo"); + router.at("/foo/bar").get(async move |_| "/foo/bar"); + + for path in &["/", "/foo", "/foo/bar"] { + let res = block_on(simulate_request(&router, path, http::Method::GET)); + let body = block_on(res.into_body().into_vec()).expect("Reading body should succeed"); + assert_eq!(&*body, path.as_bytes()); + } } -} -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())) + #[test] + fn nested_static() { + let mut router = App::new(()); + router.at("/a").get(async move |_| "/a"); + router.at("/b").nest(|router| { + router.at("/").get(async move |_| "/b"); + router.at("/a").get(async move |_| "/b/a"); + router.at("/b").get(async move |_| "/b/b"); + router.at("/c").nest(|router| { + router.at("/a").get(async move |_| "/b/c/a"); + router.at("/b").get(async move |_| "/b/c/b"); + }); + router.at("/d").get(async move |_| "/b/d"); + }); + router.at("/a/a").nest(|router| { + router.at("/a").get(async move |_| "/a/a/a"); + router.at("/b").get(async move |_| "/a/a/b"); + }); + router.at("/a/b").nest(|router| { + router.at("/").get(async move |_| "/a/b"); + }); + + for failing_path in &["/", "/a/a", "/a/b/a"] { + let 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 + ); + } + } + + 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 = block_on(simulate_request(&router, path, http::Method::GET)); + 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::new(()); + router.at("/a").nest(|router| { + router.at("/b").get(async move |_| "/a/b GET"); + }); + router.at("/a/b").post(async move |_| "/a/b POST"); + + for (path, method) in &[("/a/b", http::Method::GET), ("/a/b", http::Method::POST)] { + let res = block_on(simulate_request(&router, path, method.clone())); + assert!(res.status().is_success()); + let body = block_on(res.into_body().into_vec()).expect("Reading body should succeed"); + assert_eq!(&*body, format!("{} {}", path, method).as_bytes()); + } } } 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..aaf1c5b3a --- /dev/null +++ b/src/context.rs @@ -0,0 +1,146 @@ +use http::{HeaderMap, Method, Uri, Version}; +use http_service::Body; +use route_recognizer::Params; +use std::{str::FromStr, sync::Arc}; + +/// Data associated with a request-response lifecycle. +/// +/// The `Context` gives endpoints access to basic information about the incoming +/// request, route parameters, and various ways of accessing the request's body. +/// +/// Contexts also provide *extensions*, a type map primarily used for low-level +/// communication between middleware and endpoints. +pub struct Context { + app_data: Arc, + request: http_service::Request, + route_params: Params, +} + +impl Context { + pub(crate) fn new( + app_data: Arc, + request: http::Request, + route_params: Params, + ) -> Context { + Context { + app_data, + request, + route_params, + } + } + + /// Access the request's HTTP method. + pub fn method(&self) -> &Method { + self.request.method() + } + + /// Access the request's full URI method. + pub fn uri(&self) -> &Uri { + self.request.uri() + } + + /// Access the request's HTTP version. + pub fn version(&self) -> Version { + self.request.version() + } + + /// Access the request's headers. + pub fn headers(&self) -> &HeaderMap { + self.request.headers() + } + + /// Access the entire request. + pub fn request(&self) -> &http_service::Request { + &self.request + } + + /// Mutably access the request body + pub fn body(&mut self) -> &mut http_service::Body { + self.request.body_mut() + } + + /// Access app-global data. + pub fn app_data(&self) -> &AppData { + &self.app_data + } + + /// Extract and parse a route parameter by name. + /// + /// Returns the results of parsing the parameter according to the inferred + /// output type `T`. + /// + /// The name should *not* include the leading `:` or the trailing `*` (if + /// any). + /// + /// # Errors + /// + /// Yields an `Err` if the parameter was found but failed to parse as an + /// instance of type `T`. + /// + /// # Panics + /// + /// Panic if `key` is not a parameter for the route. + pub fn param(&self, key: &str) -> Result { + self.route_params.find(key).unwrap().parse() + } + + /// Reads the entire request body into a byte buffer. + /// + /// This method can be called after the body has already been read, but will + /// produce an empty buffer. + /// + /// # Errors + /// + /// Any I/O error encountered while reading the body is immediately returned + /// as an `Err`. + pub async fn body_bytes(&mut self) -> std::io::Result> { + await!(self.take_body().into_vec()) + } + + /// Reads the entire request body into a string. + /// + /// This method can be called after the body has already been read, but will + /// produce an empty buffer. + /// + /// # Errors + /// + /// Any I/O error encountered while reading the body is immediately returned + /// as an `Err`. + /// + /// If the body cannot be interpreted as valid UTF-8, an `Err` is returned. + 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)?) + } + + /// Reads and deserialized the entire request body via json. + /// + /// # Errors + /// + /// Any I/O error encountered while reading the body is immediately returned + /// as an `Err`. + /// + /// If the body cannot be interpreted as valid json for the target type `T`, + /// an `Err` is returned. + 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)?) + } + + /// Remove ownership of the request body, replacing it with an empty body. + /// + /// Used primarily for working directly with the body stream. + pub fn take_body(&mut self) -> Body { + std::mem::replace(self.request.body_mut(), Body::empty()) + } + + /// Access the extensions to the context. + pub fn extensions(&self) -> &http::Extensions { + self.request.extensions() + } + + /// Mutably access the extensions to the context. + pub fn extensions_mut(&mut self) -> &mut http::Extensions { + self.request.extensions_mut() + } +} diff --git a/src/cookies.rs b/src/cookies.rs index 7d3cca79c..fd88c6cda 100644 --- a/src/cookies.rs +++ b/src/cookies.rs @@ -1,44 +1,37 @@ 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 + .extensions_mut() + .remove() + .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.extensions_mut().insert(cookie_data); + + cookie } } diff --git a/src/endpoint.rs b/src/endpoint.rs index 6b3c9295c..bbfbedc08 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -1,13 +1,15 @@ 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. +/// A Tide endpoint. /// -/// This trait is automatically implemented by a host of `Fn` types, and should not be -/// implemented directly by Tide users. +/// This trait is automatically implemented for `Fn` types, and so is rarely implemented +/// directly by Tide users. +/// +/// In practice, endpoints are function that take a `Context` as an argument and +/// return a type `T` that implements [`IntoResponse`]. /// /// # Examples /// @@ -20,154 +22,53 @@ use crate::{ /// /// ```rust, no_run /// # #![feature(async_await, futures_api)] -/// async fn hello() -> String { +/// async fn hello(_cx: tide::Context<()>) -> String { /// String::from("hello") /// } /// /// fn main() { /// let mut app = tide::App::new(()); /// app.at("/hello").get(hello); -/// app.serve() -/// } -/// ``` -/// -/// Endpoint accessing `App` state (`Data`) and body of `POST` request as `String`: -/// -/// ```rust, no_run -/// # #![feature(async_await, futures_api)] -/// use std::sync::Arc; -/// use std::sync::Mutex; -/// use tide::AppData; -/// use tide::body; -/// -/// #[derive(Clone, Default)] -/// struct Database { -/// contents: Arc>>, -/// } -/// -/// async fn insert( -/// mut db: AppData, -/// msg: body::Str, -/// ) -> String { -/// // insert into db -/// # String::from("") -/// } -/// -/// fn main() { -/// let mut app = tide::App::new(Database::default()); -/// app.at("/messages/insert").post(insert); -/// app.serve() +/// app.serve("127.0.0.1:8000").unwrap() /// } /// ``` -/// -/// 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; -} - -type BoxedEndpointFn = - dyn Fn(Data, Request, Option, &Store) -> FutureObj<'static, Response> + Send + Sync; - -pub(crate) struct BoxedEndpoint { - endpoint: Box>, + /// Invoke the endpoint within the given context + fn call(&self, cx: Context) -> Self::Fut; } -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(crate) type DynEndpoint = + dyn (Fn(Context) -> FutureObj<'static, Response>) + 'static + Send + Sync; - 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),*) - }; +/// The future retured by the endpoint implementation for `Fn` types. +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>; - - #[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),*)); +impl Future for ResponseWrapper +where + F: Future, + F::Output: IntoResponse, +{ + type Output = Response; - 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/error.rs b/src/error.rs new file mode 100644 index 000000000..4daeab999 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,90 @@ +use http::{HttpTryFrom, Response, StatusCode}; +use http_service::Body; + +use crate::response::IntoResponse; + +pub(crate) type BoxTryFuture = futures::future::FutureObj<'static, EndpointResult>; + +/// A convenient `Result` instantiation appropriate for most endpoints. +pub type EndpointResult> = Result; + +#[derive(Debug)] +pub struct StringError(pub String); +impl std::error::Error for StringError {} + +impl std::fmt::Display for StringError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + self.0.fmt(f) + } +} + +macro_rules! err_fmt { + {$($t:tt)*} => { + crate::error::StringError(format!($($t)*)) + } +} + +/// A generic endpoint error, which can be converted into a response. +pub struct Error { + resp: Response, +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + self.resp + } +} + +struct Cause(Box); + +impl From> for Error { + fn from(resp: Response) -> Error { + Error { resp } + } +} + +impl From for Error { + fn from(status: StatusCode) -> Error { + let resp = Response::builder() + .status(status) + .body(Body::empty()) + .unwrap(); + Error { resp } + } +} + +/// Extends the `Result` type with convenient methods for constructing Tide errors. +pub trait ResultExt: Sized { + /// Convert to an `EndpointResult`, treating the `Err` case as a client + /// error (response code 400). + fn client_err(self) -> EndpointResult { + self.with_err_status(400) + } + + /// Convert to an `EndpointResult`, treating the `Err` case as a server + /// error (response code 500). + fn server_err(self) -> EndpointResult { + self.with_err_status(500) + } + + /// Convert to an `EndpointResult`, wrapping the `Err` case with a custom + /// response status. + fn with_err_status(self, status: S) -> EndpointResult + where + StatusCode: HttpTryFrom; +} + +impl ResultExt for std::result::Result { + fn with_err_status(self, status: S) -> EndpointResult + where + StatusCode: HttpTryFrom, + { + self.map_err(|e| Error { + resp: Response::builder() + .status(status) + .extension(Cause(Box::new(e))) + .body(Body::empty()) + .unwrap(), + }) + } +} 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..277cdc457 --- /dev/null +++ b/src/forms.rs @@ -0,0 +1,55 @@ +use futures::future::FutureObj; +use http_service::Body; +use multipart::server::Multipart; +use std::io::Cursor; + +use crate::{ + error::{BoxTryFuture, ResultExt}, + Context, Response, +}; + +/// An extension trait for `Context`, providing form extraction. +pub trait ExtractForms { + /// Asynchronously extract the entire body as a single form. + fn body_form(&mut self) -> BoxTryFuture; + + /// Asynchronously extract the entire body as a multipart form. + fn body_multipart(&mut self) -> BoxTryFuture>>>; +} + +impl ExtractForms for Context { + fn body_form(&mut self) -> BoxTryFuture { + let body = self.take_body(); + box_async! { + let body = await!(body.into_vec()).client_err()?; + Ok(serde_qs::from_bytes(&body).map_err(|e| err_fmt!("could not decode form: {}", e)).client_err()?) + } + } + + fn body_multipart(&mut self) -> 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.take_body(); + + box_async! { + let body = await!(body.into_vec()).client_err()?; + let boundary = boundary.ok_or(err_fmt!("no boundary found")).client_err()?; + Ok(Multipart::with_body(Cursor::new(body), boundary)) + } + } +} + +/// Encode `t` as a form response. +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..ea4c7d39f 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,29 +11,31 @@ //! The [`App`](struct.App.html) docs are a good place to get started. //! //! + +macro_rules! box_async { + {$($t:tt)*} => { + FutureObj::new(Box::new(async move { $($t)* })) + }; +} + +#[macro_use] +pub mod error; + 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}, + error::{EndpointResult, Error}, + response::Response, + route::Route, }; -pub use path_table::RouteMatch; diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index e5c42d139..8be5645ae 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -5,19 +5,25 @@ use http::{ HeaderMap, HttpTryFrom, }; -use crate::{middleware::RequestContext, Middleware, Response}; +use crate::{ + middleware::{Middleware, Next}, + Context, Response, +}; +/// Middleware for providing a set of default headers for all responses. #[derive(Clone, Default)] pub struct DefaultHeaders { headers: HeaderMap, } impl DefaultHeaders { + /// Construct a new instance with an empty list of headers. pub fn new() -> DefaultHeaders { DefaultHeaders::default() } #[inline] + /// Add a header to the default header list. pub fn header(mut self, key: K, value: V) -> Self where K: IntoHeaderName, @@ -33,18 +39,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..23f8823f0 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,65 +1,46 @@ -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>; + fn handle<'a>( + &'a self, + cx: Context, + next: Next<'a, AppData>, + ) -> FutureObj<'a, Response>; } impl Middleware for F where - F: Send + Sync + Fn(RequestContext) -> FutureObj, + F: Send + Sync + 'static + for<'a> Fn(Context, Next<'a, Data>) -> FutureObj<'a, Response>, { - fn handle<'a>(&'a self, ctx: RequestContext<'a, Data>) -> FutureObj<'a, Response> { - (self)(ctx) + fn handle<'a>(&'a self, cx: Context, next: Next<'a, Data>) -> FutureObj<'a, Response> { + (self)(cx, next) } } -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>], +/// The remainder of a middleware chain, including the endpoint. +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> { + /// Asynchronously execute the remaining middleware chain. + 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..854d5ba06 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,11 +1,16 @@ 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; + +/// Serialize `t` into a JSON-encoded 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 { @@ -15,7 +20,7 @@ pub trait IntoResponse: Send + Sized { /// Create a new `IntoResponse` value that will respond with the given status code. /// /// ``` - /// # use tide::IntoResponse; + /// # use tide::response::IntoResponse; /// let resp = "Hello, 404!".with_status(http::status::StatusCode::NOT_FOUND).into_response(); /// assert_eq!(resp.status(), http::status::StatusCode::NOT_FOUND); /// ``` @@ -46,12 +51,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..21175850b --- /dev/null +++ b/src/route.rs @@ -0,0 +1,101 @@ +use crate::{router::Router, Endpoint}; + +/// A handle to a route. +/// +/// All HTTP requests are made against resources. After using [`App::at`] (or +/// [`Route::at`]) to establish a route, the `Route` 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. +pub struct Route<'a, AppData> { + router: &'a mut Router, + path: String, +} + +impl<'a, AppData: 'static> Route<'a, AppData> { + pub(crate) fn new(router: &'a mut Router, path: String) -> Route<'a, AppData> { + Route { router, path } + } + + /// Extend the route with the given `path`. + pub fn at<'b>(&'b mut self, path: &str) -> Route<'b, AppData> { + let mut p = self.path.clone(); + + if !p.ends_with('/') && !path.starts_with('/') { + p.push_str("/"); + } + + if path != "/" { + p.push_str(path); + } + + Route { + router: &mut self.router, + path: p, + } + } + + pub fn nest(&mut self, f: impl FnOnce(&mut Route<'a, AppData>)) -> &mut Self { + f(self); + self + } + + /// 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..d403241f5 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,633 +1,70 @@ -use std::any::Any; -use std::collections::HashMap; -use std::fmt::Debug; -use std::sync::Arc; +use fnv::FnvHashMap; +use futures::future::FutureObj; +use http_service::Body; +use route_recognizer::{Match, Params, Router as MethodRouter}; use crate::{ - configuration::Store, - endpoint::{BoxedEndpoint, Endpoint}, - Middleware, + endpoint::{DynEndpoint, Endpoint}, + Context, Response, }; -use path_table::{PathTable, RouteMatch}; -/// 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 Selection<'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()); + method_map: FnvHashMap::default(), } } - #[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()); - } + 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 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()); - } - } - - #[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) - ); - } - - #[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) -> Selection<'_, AppData> { + if let Some(Match { handler, params }) = self + .method_map + .get(&method) + .and_then(|r| r.recognize(path).ok()) + { + Selection { + endpoint: &**handler, + params, } - } + } 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 - // 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 + self.route(path, http::Method::GET) + } else { + Selection { + endpoint: ¬_found_endpoint, + params: Params::new(), } - }); - 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 - - 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() - } - - 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"); +fn not_found_endpoint(_cx: Context) -> FutureObj<'static, Response> { + box_async! { + http::Response::builder().status(http::StatusCode::NOT_FOUND).body(Body::empty()).unwrap() } } 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); -} diff --git a/tests/wildcard.rs b/tests/wildcard.rs old mode 100644 new mode 100755 index 8e9c0d173..f0de6278a --- a/tests/wildcard.rs +++ b/tests/wildcard.rs @@ -3,29 +3,17 @@ use futures::executor::block_on; use http_service::Body; use http_service_mock::make_server; -use tide::head::{Named, NamedSegment}; +use tide::{error::ResultExt, Context}; -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 add_one(Named(Number(num)): Named) -> String { - (num + 1).to_string() +async fn add_one(cx: Context<()>) -> Result { + let num: i64 = cx.param("num").client_err()?; + Ok((num + 1).to_string()) } #[test] fn wildcard() { let mut app = tide::App::new(()); - app.at("/add_one/{num}").get(add_one); + app.at("/add_one/:num").get(add_one); let mut server = make_server(app.into_http_service()).unwrap(); let req = http::Request::get("/add_one/3") @@ -48,7 +36,7 @@ fn wildcard() { #[test] fn invalid_segment_error() { let mut app = tide::App::new(()); - app.at("/add_one/{num}").get(add_one); + app.at("/add_one/:num").get(add_one); let mut server = make_server(app.into_http_service()).unwrap(); let req = http::Request::get("/add_one/a") @@ -61,7 +49,7 @@ fn invalid_segment_error() { #[test] fn not_found_error() { let mut app = tide::App::new(()); - app.at("/add_one/{num}").get(add_one); + app.at("/add_one/:num").get(add_one); let mut server = make_server(app.into_http_service()).unwrap(); let req = http::Request::get("/add_one/").body(Body::empty()).unwrap();