diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e3ad56ae0..a11172a7f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,7 +11,6 @@ Repository. - [Code of Conduct](#code-of-conduct) - [Bad Actors](#bad-actors) -- [Developer Certificate of Origin](#developer-certificate-of-origin) ## Code of Conduct The project has a [Code of Conduct](./CODE_OF_CONDUCT.md) that *all* @@ -54,10 +53,3 @@ contributors the benefit of the doubt and having a sincere willingness to admit that you *might* be wrong is critical for any successful open collaboration. Don't be a bad actor. - -## Developer Certificate of Origin -All contributors must read and agree to the [Developer Certificate of -Origin (DCO)](../CERTIFICATE). - -The DCO allows us to accept contributions from people to the project, similarly -to how a license allows us to distribute our code. diff --git a/.travis.yml b/.travis.yml index 3e4d10906..5a15187c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,56 @@ language: rust -rust: - - nightly-2019-05-09 - -before_script: | - rustup component add rustfmt clippy -script: | - cargo fmt --all -- --check && - cargo clippy --all -- -D clippy::all && - cargo build --no-default-features --verbose && - cargo build --all --verbose && - cargo test --all --verbose +rust: nightly-2019-05-09 cache: cargo + +before_script: +- > + [[ "$(cargo-sweep --version)" == "cargo-sweep 0.4.1" ]] + || cargo install cargo-sweep +- cargo sweep --stamp + +before_cache: +- cargo sweep --file + +matrix: + include: + - name: cargo doc + env: [CACHE_NAME=docs] + script: + - RUSTDOCFLAGS=-Dwarnings cargo doc + -Zmtime-on-use + --all --all-features + --exclude tide + --no-deps + + - name: cargo fmt + cache: false + before_script: [] + install: + - rustup component add rustfmt + script: + - cargo fmt --all -- --check + + - name: cargo clippy + env: [CACHE_NAME=clippy] + install: + - rustup component add clippy + script: + - cargo clippy + -Zmtime-on-use + --all --all-targets + -- -Dwarnings + + - name: cargo build --no-default-features + env: [CACHE_NAME=no-default-features] + script: + - cargo build + -Zmtime-on-use + --manifest-path tide-core/Cargo.toml + --no-default-features + - cargo build + -Zmtime-on-use + --no-default-features + + - name: cargo test + script: + - cargo test -Zmtime-on-use --all --verbose diff --git a/CERTIFICATE b/CERTIFICATE deleted file mode 100644 index 8201f9921..000000000 --- a/CERTIFICATE +++ /dev/null @@ -1,37 +0,0 @@ -Developer Certificate of Origin -Version 1.1 - -Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -1 Letterman Drive -Suite D4700 -San Francisco, CA, 94129 - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - - -Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -(a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -(b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -(d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. diff --git a/Cargo.toml b/Cargo.toml index d90daaea7..133e63621 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,48 +18,50 @@ readme = "README.md" repository = "https://github.com/rustasync/tide" version = "0.2.0" +[features] +default = ["hyper", "cookies"] +cookies = ["tide-cookies"] +hyper = ["tide-core/http-service-hyper"] + [dependencies] -cookie = { version = "0.12", features = ["percent-encode"] } futures-preview = "0.3.0-alpha.16" -fnv = "1.0.6" http = "0.1" http-service = "0.2.0" -pin-utils = "0.1.0-alpha.4" -route-recognizer = "0.1.12" -serde = "1.0.91" -serde_derive = "1.0.91" -serde_json = "1.0.39" -slog = "2.4.1" -slog-async = "2.3.0" -slog-term = "2.4.0" -typemap = "0.3.3" -serde_urlencoded = "0.5.5" - -[dependencies.http-service-hyper] -optional = true -version = "0.2.0" - -[dependencies.multipart] -default-features = false -features = ["server"] -version = "0.16.1" - -[features] -default = ["hyper"] -hyper = ["http-service-hyper"] +tide-cookies = { path = "./tide-cookies", optional = true } +tide-core = { path = "./tide-core" } +tide-headers = { path = "./tide-headers" } +tide-log = { path = "./tide-log" } +tide-forms = { path = "./tide-forms" } +tide-querystring = { path = "./tide-querystring" } [dev-dependencies] -basic-cookies = "0.1.3" bytes = "0.4.12" +cookie = { version = "0.12", features = ["percent-encode"] } futures-fs = "0.0.5" futures-util-preview = { version = "0.3.0-alpha.16", features = ["compat"] } http-service-mock = "0.2.0" -juniper = "0.11.1" +juniper = "0.12.0" mime = "0.3.13" mime_guess = "2.0.0-alpha.6" percent-encoding = "1.0.1" -serde = { version = "1.0.90", features = ["derive"] } -structopt = "0.2.15" +serde = { version = "1.0.91", features = ["derive"] } +tide-log = { path = "./tide-log" } +env_logger = "0.6.1" +log4rs = "0.8.3" +log = "0.4.6" + +[workspace] +members = [ + "tide-compression", + "tide-cookies", + "tide-sessions", + "tide-core", + "tide-forms", + "tide-headers", + "tide-log", + "tide-querystring", + "tide-slog", +] [patch.crates-io] http-service = { git = "https://github.com/rustasync/http-service", branch = "master" } diff --git a/README.md b/README.md index 548b64ba2..f8297ef00 100644 --- a/README.md +++ b/README.md @@ -59,26 +59,27 @@ Ecosystem WG, and **not ready for production use yet**. **Hello World** -```rust +```rust,no_run #![feature(async_await)] fn main() -> Result<(), std::io::Error> { let mut app = tide::App::new(); app.at("/").get(async move |_| "Hello, world!"); - Ok(app.serve("127.0.0.1:8000")?) + Ok(app.run("127.0.0.1:8000")?) } ``` **More Examples** -- [Hello World](https://github.com/rustasync/tide/tree/master/examples/hello.rs) +- [Hello World](https://github.com/rustasync/tide/blob/master/examples/hello.rs) - [Messages](https://github.com/rustasync/tide/blob/master/examples/messages.rs) - [Body Types](https://github.com/rustasync/tide/blob/master/examples/body_types.rs) -- [Multipart Form](https://github.com/rustasync/tide/tree/master/examples/multipart-form/main.rs) -- [Catch All](https://github.com/rustasync/tide/tree/master/examples/catch_all.rs) -- [Cookies](https://github.com/rustasync/tide/tree/master/examples/cookies.rs) -- [Default Headers](https://github.com/rustasync/tide/tree/master/examples/default_headers.rs) -- [GraphQL](https://github.com/rustasync/tide/tree/master/examples/graphql.rs) +- [Multipart Form](https://github.com/rustasync/tide/blob/master/examples/multipart_form/mod.rs) +- [Catch All](https://github.com/rustasync/tide/blob/master/examples/catch_all.rs) +- [Cookies](https://github.com/rustasync/tide/blob/master/examples/cookies.rs) +- [Default Headers](https://github.com/rustasync/tide/blob/master/examples/default_headers.rs) +- [GraphQL](https://github.com/rustasync/tide/blob/master/examples/graphql.rs) +- [Staticfile](https://github.com/rustasync/tide/blob/master/examples/staticfile.rs) ## Resources diff --git a/examples/body_types.rs b/examples/body_types.rs index 01c9b393c..18269bfb0 100644 --- a/examples/body_types.rs +++ b/examples/body_types.rs @@ -1,9 +1,8 @@ #![feature(async_await)] - use serde::{Deserialize, Serialize}; use tide::{ error::ResultExt, - forms::{self, ExtractForms}, + forms::{self, ContextExt}, response, App, Context, EndpointResult, }; @@ -13,30 +12,26 @@ struct Message { contents: String, } -#[allow(unused_mut)] // Workaround clippy bug async fn echo_string(mut cx: Context<()>) -> String { let msg = cx.body_string().await.unwrap(); println!("String: {}", msg); msg } -#[allow(unused_mut)] // Workaround clippy bug async fn echo_bytes(mut cx: Context<()>) -> Vec { let msg = cx.body_bytes().await.unwrap(); println!("Bytes: {:?}", msg); msg } -#[allow(unused_mut)] // Workaround clippy bug async fn echo_json(mut cx: Context<()>) -> EndpointResult { - let msg = cx.body_json().await.client_err()?; + let msg: Message = cx.body_json().await.client_err()?; println!("JSON: {:?}", msg); Ok(response::json(msg)) } -#[allow(unused_mut)] // Workaround clippy bug async fn echo_form(mut cx: Context<()>) -> EndpointResult { - let msg = cx.body_form().await?; + let msg: Message = cx.body_form().await?; println!("Form: {:?}", msg); Ok(forms::form(msg)) } @@ -49,5 +44,5 @@ fn main() { app.at("/echo/json").post(echo_json); app.at("/echo/form").post(echo_form); - app.serve("127.0.0.1:8000").unwrap(); + app.run("127.0.0.1:8000").unwrap(); } diff --git a/examples/catch_all.rs b/examples/catch_all.rs index 354ddbb03..7ff1980d5 100644 --- a/examples/catch_all.rs +++ b/examples/catch_all.rs @@ -1,5 +1,4 @@ #![feature(async_await)] - use tide::Context; async fn echo_path(cx: Context<()>) -> String { @@ -10,5 +9,5 @@ async fn echo_path(cx: Context<()>) -> String { fn main() { let mut app = tide::App::new(); app.at("/echo_path/*path").get(echo_path); - app.serve("127.0.0.1:8000").unwrap(); + app.run("127.0.0.1:8000").unwrap(); } diff --git a/examples/cookies.rs b/examples/cookies.rs index 094030f27..92820e6a8 100644 --- a/examples/cookies.rs +++ b/examples/cookies.rs @@ -1,20 +1,16 @@ #![feature(async_await)] - use cookie::Cookie; use tide::{cookies::ContextExt, middleware::CookiesMiddleware, Context}; /// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter. -/// async fn retrieve_cookie(mut cx: Context<()>) -> String { format!("hello cookies: {:?}", cx.get_cookie("hello").unwrap()) } -#[allow(unused_mut)] // Workaround clippy bug async fn set_cookie(mut cx: Context<()>) { cx.set_cookie(Cookie::new("hello", "world")).unwrap(); } -#[allow(unused_mut)] // Workaround clippy bug async fn remove_cookie(mut cx: Context<()>) { cx.remove_cookie(Cookie::named("hello")).unwrap(); } @@ -26,5 +22,5 @@ fn main() { app.at("/").get(retrieve_cookie); app.at("/set").get(set_cookie); app.at("/remove").get(remove_cookie); - app.serve("127.0.0.1:8000").unwrap(); + app.run("127.0.0.1:8000").unwrap(); } diff --git a/examples/default_headers.rs b/examples/default_headers.rs index 47f13a091..70f4a8d54 100644 --- a/examples/default_headers.rs +++ b/examples/default_headers.rs @@ -13,5 +13,5 @@ fn main() { app.at("/").get(async move |_| "Hello, world!"); - app.serve("127.0.0.1:8000").unwrap(); + app.run("127.0.0.1:8000").unwrap(); } diff --git a/examples/graphql.rs b/examples/graphql.rs index aeb9abfdf..f79527c12 100644 --- a/examples/graphql.rs +++ b/examples/graphql.rs @@ -2,27 +2,25 @@ // a look at [the Juniper book]. // // [the Juniper book]: https://graphql-rust.github.io/ - #![feature(async_await)] - use http::status::StatusCode; use juniper::graphql_object; use std::sync::{atomic, Arc}; use tide::{error::ResultExt, response, App, Context, EndpointResult}; -// First, we define `Data` that holds accumulator state. This is accessible as App data in +// First, we define `State` that holds accumulator state. This is accessible as state in // Tide, and as executor context in Juniper. #[derive(Clone, Default)] -struct Data(Arc); +struct State(Arc); -impl juniper::Context for Data {} +impl juniper::Context for State {} // 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 +// doesn't have any associated state (and there's no need to do so), but instead it exposes the // accumulator state from the context. struct Query; -graphql_object!(Query: Data |&self| { +graphql_object!(Query: State |&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 +31,7 @@ graphql_object!(Query: Data |&self| { // `Query`, but it provides the way to "mutate" the accumulator state. struct Mutation; -graphql_object!(Mutation: Data |&self| { +graphql_object!(Mutation: State |&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,7 +43,7 @@ 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(mut cx: Context) -> EndpointResult { +async fn handle_graphql(mut cx: Context) -> EndpointResult { let query: juniper::http::GraphQLRequest = cx.body_json().await.client_err()?; let schema = Schema::new(Query, Mutation); let response = query.execute(&schema, cx.state()); @@ -60,7 +58,7 @@ async fn handle_graphql(mut cx: Context) -> EndpointResult { } fn main() { - let mut app = App::with_state(Data::default()); + let mut app = App::with_state(State::default()); app.at("/graphql").post(handle_graphql); - app.serve("127.0.0.1:8000").unwrap(); + app.run("127.0.0.1:8000").unwrap(); } diff --git a/examples/hello.rs b/examples/hello.rs index 030d394c1..edcf106e2 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,7 +1,6 @@ #![feature(async_await)] - fn main() { let mut app = tide::App::new(); app.at("/").get(async move |_| "Hello, world!"); - app.serve("127.0.0.1:8000").unwrap(); + app.run("127.0.0.1:8000").unwrap(); } diff --git a/examples/hello_envlog.rs b/examples/hello_envlog.rs new file mode 100644 index 000000000..d57a5a4bd --- /dev/null +++ b/examples/hello_envlog.rs @@ -0,0 +1,8 @@ +#![feature(async_await)] +fn main() { + env_logger::from_env(env_logger::Env::default().default_filter_or("info")).init(); + let mut app = tide::App::new(); + app.middleware(tide::middleware::RequestLogger::new()); + app.at("/").get(async move |_| "Hello, world!"); + app.run("127.0.0.1:8000").unwrap(); +} diff --git a/examples/hello_logrs.rs b/examples/hello_logrs.rs new file mode 100644 index 000000000..d32f2696e --- /dev/null +++ b/examples/hello_logrs.rs @@ -0,0 +1,18 @@ +#![feature(async_await)] +fn main() { + use log::LevelFilter; + use log4rs::append::console::ConsoleAppender; + use log4rs::config::{Appender, Config, Root}; + + let stdout = ConsoleAppender::builder().build(); + let config = Config::builder() + .appender(Appender::builder().build("stdout", Box::new(stdout))) + .build(Root::builder().appender("stdout").build(LevelFilter::Info)) + .unwrap(); + let _handle = log4rs::init_config(config).unwrap(); + + let mut app = tide::App::new(); + app.middleware(tide::middleware::RequestLogger::new()); + app.at("/").get(async move |_| "Hello, world!"); + app.run("127.0.0.1:8000").unwrap(); +} diff --git a/examples/messages.rs b/examples/messages.rs index 7b8ef25dd..023528c89 100644 --- a/examples/messages.rs +++ b/examples/messages.rs @@ -39,13 +39,11 @@ impl Database { } } -#[allow(unused_mut)] // Workaround clippy bug async fn new_message(mut cx: Context) -> EndpointResult { let msg = cx.body_json().await.client_err()?; Ok(cx.state().insert(msg).to_string()) } -#[allow(unused_mut)] // Workaround clippy bug async fn set_message(mut cx: Context) -> EndpointResult<()> { let msg = cx.body_json().await.client_err()?; let id = cx.param("id").client_err()?; @@ -70,5 +68,5 @@ fn main() { let mut app = App::with_state(Database::default()); app.at("/message").post(new_message); app.at("/message/:id").get(get_message).post(set_message); - app.serve("127.0.0.1:8000").unwrap(); + app.run("127.0.0.1:8000").unwrap(); } diff --git a/examples/multipart-form/main.rs b/examples/multipart_form/mod.rs similarity index 92% rename from examples/multipart-form/main.rs rename to examples/multipart_form/mod.rs index 14ca63639..0abef7c48 100644 --- a/examples/multipart-form/main.rs +++ b/examples/multipart_form/mod.rs @@ -1,8 +1,7 @@ #![feature(async_await)] - use serde::{Deserialize, Serialize}; use std::io::Read; -use tide::{forms::ExtractForms, response, App, Context, EndpointResult}; +use tide::{forms::ContextExt, response, App, Context, EndpointResult}; #[derive(Serialize, Deserialize, Clone)] struct Message { @@ -11,7 +10,6 @@ struct Message { file: Option, } -#[allow(unused_mut)] // Workaround clippy bug 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 { @@ -57,10 +55,10 @@ async fn upload_file(mut cx: Context<()>) -> EndpointResult { Ok(response::json(message)) } -fn main() { +pub fn run() { let mut app = App::new(); app.at("/upload_file").post(upload_file); - app.serve("127.0.0.1:8000").unwrap(); + app.run("127.0.0.1:8000").unwrap(); } // Test with: diff --git a/examples/multipart-form/test.txt b/examples/multipart_form/test.txt similarity index 100% rename from examples/multipart-form/test.txt rename to examples/multipart_form/test.txt diff --git a/examples/staticfile.rs b/examples/staticfile.rs index 462a1f4bd..d712aceb8 100644 --- a/examples/staticfile.rs +++ b/examples/staticfile.rs @@ -44,7 +44,7 @@ impl StaticFile { // Check if the path exists and handle if it's a directory containing `index.html` if meta.is_some() && meta.as_ref().map(|m| !m.is_file()).unwrap_or(false) { // Redirect if path is a dir and URL doesn't end with "/" - if !actual_path.ends_with("/") { + if !actual_path.ends_with('/') { return Ok(response .status(StatusCode::MOVED_PERMANENTLY) .header(header::LOCATION, String::from(actual_path) + "/") @@ -124,5 +124,5 @@ async fn handle_path(ctx: Context) -> EndpointResult { fn main() { let mut app = App::with_state(StaticFile::new("./")); app.at("/*").get(handle_path); - app.serve("127.0.0.1:8000").unwrap(); + app.run("127.0.0.1:8000").unwrap(); } diff --git a/src/lib.rs b/src/lib.rs index ac931431d..5b8ef56ac 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,13 @@ -#![cfg_attr(feature = "nightly", deny(missing_docs))] -#![cfg_attr(feature = "nightly", feature(external_doc))] +#![cfg_attr(any(feature = "nightly", test), feature(external_doc))] #![cfg_attr(feature = "nightly", doc(include = "../README.md"))] -#![cfg_attr(test, deny(warnings))] #![feature(async_await, existential_type)] -#![allow(unused_variables)] -#![deny( +#![warn( nonstandard_style, rust_2018_idioms, future_incompatible, - missing_debug_implementations + missing_debug_implementations, + missing_docs )] -// TODO: Remove this after clippy bug due to async await is resolved. -// ISSUE: https://github.com/rust-lang/rust-clippy/issues/3988 -#![allow(clippy::needless_lifetimes)] //! //! Welcome to Tide. @@ -21,34 +16,52 @@ //! //! -macro_rules! box_async { - {$($t:tt)*} => { - ::futures::future::FutureExt::boxed(async move { $($t)* }) - }; -} +#[cfg(test)] +#[doc(include = "../README.md")] +const _README: () = (); -#[macro_use] -pub mod error; +pub use http; -mod app; -mod context; -pub mod cookies; -mod endpoint; -pub mod forms; -pub mod middleware; -pub mod querystring; -pub mod response; -mod route; -mod router; +#[cfg(feature = "cookies")] +#[doc(inline)] +pub use tide_cookies as cookies; #[doc(inline)] -pub use crate::{ - app::{App, Server}, - context::Context, - endpoint::Endpoint, - error::{EndpointResult, Error}, - response::Response, - route::Route, +pub use tide_core::{ + err_fmt, + response, + App, + Context, + Endpoint, + EndpointResult, + Error, + Response, + Route, + Server, + // TODO: export Body once it's in turn exported by tide_core }; -pub use http; +pub mod error { + //! Module to export tide_core errors + + pub use tide_core::error::{ + EndpointResult, Error, ResponseExt, ResultDynErrExt, ResultExt, StringError, + }; +} + +pub use tide_forms as forms; +pub use tide_querystring as querystring; + +pub mod middleware { + //! Module to export tide_core middleware + + // Core + pub use tide_core::middleware::{Middleware, Next}; + + // Exports from tide repo. + pub use tide_headers::DefaultHeaders; + pub use tide_log::RequestLogger; + + #[cfg(feature = "cookies")] + pub use tide_cookies::CookiesMiddleware; +} diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs deleted file mode 100644 index 3becf7928..000000000 --- a/src/middleware/logger.rs +++ /dev/null @@ -1,50 +0,0 @@ -use slog::{info, o, Drain}; -use slog_async; -use slog_term; - -use futures::future::BoxFuture; - -use crate::{ - middleware::{Middleware, Next}, - Context, Response, -}; - -/// Root logger for Tide. Wraps over logger provided by slog.SimpleLogger -#[derive(Debug)] -pub struct RootLogger { - // drain: dyn slog::Drain, - inner_logger: slog::Logger, -} - -impl RootLogger { - pub fn new() -> RootLogger { - let decorator = slog_term::TermDecorator::new().build(); - let drain = slog_term::CompactFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - - let log = slog::Logger::root(drain, o!()); - RootLogger { inner_logger: log } - } -} - -impl Default for RootLogger { - fn default() -> Self { - Self::new() - } -} - -/// Stores information during request phase and logs information once the response -/// is generated. -impl Middleware for RootLogger { - fn handle<'a>(&'a self, cx: Context, next: Next<'a, Data>) -> BoxFuture<'a, Response> { - box_async! { - let path = cx.uri().path().to_owned(); - let method = cx.method().as_str().to_owned(); - - let res = next.run(cx).await; - let status = res.status(); - info!(self.inner_logger, "{} {} {}", method, path, status.as_str()); - res - } - } -} diff --git a/tests/wildcard.rs b/tests/wildcard.rs index 6d21ccf46..01c364620 100644 --- a/tests/wildcard.rs +++ b/tests/wildcard.rs @@ -10,6 +10,22 @@ async fn add_one(cx: Context<()>) -> Result { Ok((num + 1).to_string()) } +async fn add_two(cx: Context<()>) -> Result { + let one: i64 = cx.param("one").client_err()?; + let two: i64 = cx.param("two").client_err()?; + Ok((one + two).to_string()) +} + +async fn echo_path(cx: Context<()>) -> Result { + let path: String = cx.param("path").client_err()?; + Ok(path) +} + +async fn echo_empty(cx: Context<()>) -> Result { + let path: String = cx.param("").client_err()?; + Ok(path) +} + #[test] fn wildcard() { let mut app = tide::App::new(); @@ -56,3 +72,156 @@ fn not_found_error() { let res = server.simulate(req).unwrap(); assert_eq!(res.status(), 404); } + +#[test] +fn wildpath() { + let mut app = tide::App::new(); + app.at("/echo/*path").get(echo_path); + let mut server = make_server(app.into_http_service()).unwrap(); + + let req = http::Request::get("/echo/some_path") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"some_path"); + + let req = http::Request::get("/echo/multi/segment/path") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"multi/segment/path"); + + let req = http::Request::get("/echo/").body(Body::empty()).unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 404); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b""); +} + +#[test] +fn multi_wildcard() { + let mut app = tide::App::new(); + app.at("/add_two/:one/:two/").get(add_two); + let mut server = make_server(app.into_http_service()).unwrap(); + + let req = http::Request::get("/add_two/1/2/") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"3"); + + let req = http::Request::get("/add_two/-1/2/") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"1"); + let req = http::Request::get("/add_two/1") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 404); +} + +#[test] +fn wild_last_segment() { + let mut app = tide::App::new(); + app.at("/echo/:path/*").get(echo_path); + let mut server = make_server(app.into_http_service()).unwrap(); + + let req = http::Request::get("/echo/one/two") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"one"); + + let req = http::Request::get("/echo/one/two/three/four") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"one"); +} + +#[test] +fn invalid_wildcard() { + let mut app = tide::App::new(); + app.at("/echo/*path/:one/").get(echo_path); + let mut server = make_server(app.into_http_service()).unwrap(); + + let req = http::Request::get("/echo/one/two") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 404); +} + +#[test] +fn nameless_wildcard() { + let mut app = tide::App::new(); + app.at("/echo/:").get(async move |_| ""); + + let mut server = make_server(app.into_http_service()).unwrap(); + + let req = http::Request::get("/echo/one/two") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 404); + + let req = http::Request::get("/echo/one").body(Body::empty()).unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); +} + +#[test] +fn nameless_internal_wildcard() { + let mut app = tide::App::new(); + app.at("/echo/:/:path").get(echo_path); + let mut server = make_server(app.into_http_service()).unwrap(); + + let req = http::Request::get("/echo/one").body(Body::empty()).unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 404); + + let req = http::Request::get("/echo/one/two") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"two"); + + let req = http::Request::get("/echo/one/two") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"two"); +} + +#[test] +fn nameless_internal_wildcard2() { + let mut app = tide::App::new(); + app.at("/echo/:/:path").get(echo_empty); + let mut server = make_server(app.into_http_service()).unwrap(); + + let req = http::Request::get("/echo/one/two") + .body(Body::empty()) + .unwrap(); + let res = server.simulate(req).unwrap(); + assert_eq!(res.status(), 200); + let body = block_on(res.into_body().into_vec()).unwrap(); + assert_eq!(&*body, &*b"one"); +} diff --git a/tide-compression/Cargo.toml b/tide-compression/Cargo.toml new file mode 100644 index 000000000..395fff270 --- /dev/null +++ b/tide-compression/Cargo.toml @@ -0,0 +1,31 @@ +[package] +authors = [ + "Tide Developers", +] +description = "Compression-related middleware for Tide" +documentation = "https://docs.rs/tide-compression" +keywords = ["tide", "web", "async", "middleware", "compression"] +categories = ["network-programming", "compression", "asynchronous"] +edition = "2018" +license = "MIT OR Apache-2.0" +name = "tide-compression" +readme = "README.md" +repository = "https://github.com/rustasync/tide" +version = "0.1.0" + +[dependencies] +tide = { path = "../" } +accept-encoding = "0.2.0-alpha.2" +bytes = "0.4.12" +futures-preview = "0.3.0-alpha.16" +http = "0.1" +http-service = "0.2.0" + +[dependencies.async-compression] +default-features = false +features = ["stream", "gzip", "zlib", "brotli", "zstd"] +version = "0.1.0-alpha.1" + +[dev-dependencies] +tide = { path = "../" } +http-service-mock = "0.2.0" diff --git a/tide-compression/README.md b/tide-compression/README.md new file mode 100644 index 000000000..4c8eb8b10 --- /dev/null +++ b/tide-compression/README.md @@ -0,0 +1,16 @@ +# tide-compression + +This crate provides compression-related middleware for Tide. + +## Examples + +Examples are in the `/examples` folder of this crate. + +__Simple Example__ + +You can test the simple example by running `cargo run --example simple` while in this crate's directory, and then running either of the following commands: + +```console +$ curl -v http://127.0.0.1:8000/ +$ echo 'why hello there' | gzip | curl -v --compressed -H 'Content-Encoding: gzip' http://127.0.0.1:8000/echo --data-binary @- +``` diff --git a/tide-compression/examples/simple.rs b/tide-compression/examples/simple.rs new file mode 100644 index 000000000..73484eac0 --- /dev/null +++ b/tide-compression/examples/simple.rs @@ -0,0 +1,22 @@ +#![feature(async_await)] +use tide::{App, Context}; +use tide_compression::{Compression, Decompression, Encoding}; + +// Returns a portion of the lorem ipsum text. +async fn lorem_ipsum(_cx: Context<()>) -> String { + String::from("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") +} + +// Echoes the request body in bytes. +async fn echo_bytes(mut cx: Context<()>) -> Vec { + cx.body_bytes().await.unwrap() +} + +fn main() { + let mut app = App::new(); + app.at("/").get(lorem_ipsum); + app.at("/echo").post(echo_bytes); + app.middleware(Compression::with_default(Encoding::Brotli)); + app.middleware(Decompression::new()); + app.run("127.0.0.1:8000").unwrap(); +} diff --git a/tide-compression/src/lib.rs b/tide-compression/src/lib.rs new file mode 100644 index 000000000..07c3c78ee --- /dev/null +++ b/tide-compression/src/lib.rs @@ -0,0 +1,420 @@ +//! Crate that provides helpers and/or middlewares for Tide +//! related to compression. + +#![cfg_attr(feature = "nightly", feature(external_doc))] +#![cfg_attr(feature = "nightly", doc(include = "../README.md"))] +#![feature(async_await)] +#![warn( + nonstandard_style, + rust_2018_idioms, + future_incompatible, + missing_debug_implementations, + missing_docs +)] + +//! Compression-related middleware for Tide + +pub use accept_encoding::Encoding; +use async_compression::stream; +use futures::future::BoxFuture; +use futures::prelude::*; +use http::{header::CONTENT_ENCODING, status::StatusCode, HeaderMap}; +use http_service::{Body, Request}; +use tide::{ + middleware::{Middleware, Next}, + response::IntoResponse, + Context, Error, Response, +}; + +/// Encode settings for the compression middleware. +/// +/// This can be modified in the case that you want more control over the speed or quality of compression. +/// +/// For more information on how to configure each of these settings, see the async-compression crate. +#[derive(Debug)] +pub struct EncodeSettings { + /// Settings for gzip compression. + pub gzip: async_compression::flate2::Compression, + /// Settings for deflate compression. + pub deflate: async_compression::flate2::Compression, + /// Settings for brotli compression. Ranges from 0-11. (default: `11`) + pub brotli: u32, + /// Settings for zstd compression. Ranges from 1-21. (default: `3`) + pub zstd: i32, +} + +impl Default for EncodeSettings { + fn default() -> Self { + Self { + gzip: Default::default(), + deflate: Default::default(), + brotli: 11, + zstd: 3, + } + } +} + +/// Middleware for automatically handling outgoing response compression. +/// +/// This middleware currently supports HTTP compression using `gzip`, `deflate`, `br`, and `zstd`. +#[derive(Debug)] +pub struct Compression { + default_encoding: Encoding, + settings: EncodeSettings, +} + +impl Default for Compression { + fn default() -> Self { + Self::new() + } +} + +impl Compression { + /// Creates a new Compression middleware. The default encoding is [`Encoding::Identity`] (no encoding). + pub fn new() -> Self { + Self { + default_encoding: Encoding::Identity, + settings: Default::default(), + } + } + + /// Creates a new Compression middleware with a provided default encoding. + /// + /// This encoding will be selected if the client has not set the `Accept-Encoding` header or `*` is set as the most preferred encoding. + pub fn with_default(default_encoding: Encoding) -> Self { + Self { + default_encoding, + settings: Default::default(), + } + } + + /// Accesses a mutable handle to this middleware's [`EncodeSettings`]. + /// + /// This will allow you to configure this middleware's settings. + pub fn settings_mut(&mut self) -> &mut EncodeSettings { + &mut self.settings + } + + fn preferred_encoding(&self, headers: &HeaderMap) -> Result { + let encoding = match accept_encoding::parse(headers) { + Ok(encoding) => encoding, + Err(_) => return Err(Error::from(StatusCode::BAD_REQUEST)), + }; + Ok(encoding.unwrap_or(self.default_encoding)) + } + + /// Consumes the response and returns an encoded version of it. + fn encode(&self, mut res: Response, encoding: Encoding) -> Response { + if res.headers().get(CONTENT_ENCODING).is_some() || encoding == Encoding::Identity { + return res; // avoid double-encoding a given response + } + let body = std::mem::replace(res.body_mut(), Body::empty()); + match encoding { + Encoding::Gzip => { + let stream = stream::GzipEncoder::new(body, self.settings.gzip); + *res.body_mut() = Body::from_stream(stream); + } + Encoding::Deflate => { + let stream = stream::ZlibEncoder::new(body, self.settings.deflate); + *res.body_mut() = Body::from_stream(stream); + } + Encoding::Brotli => { + let stream = stream::BrotliEncoder::new(body, self.settings.brotli); + *res.body_mut() = Body::from_stream(stream); + } + Encoding::Zstd => { + let stream = stream::ZstdEncoder::new(body, self.settings.zstd); + *res.body_mut() = Body::from_stream(stream); + } + Encoding::Identity => unreachable!(), + }; + res.headers_mut() + .append(CONTENT_ENCODING, encoding.to_header_value()); + res + } +} + +impl Middleware for Compression { + fn handle<'a>(&'a self, cx: Context, next: Next<'a, State>) -> BoxFuture<'a, Response> { + FutureExt::boxed(async move { + let encoding = match self.preferred_encoding(cx.headers()) { + Ok(encoding) => encoding, + Err(e) => return e.into_response(), + }; + let res = next.run(cx).await; + self.encode(res, encoding) + }) + } +} + +/// Middleware for handling incoming request decompression. +/// +/// This middleware currently supports HTTP decompression under the `gzip`, `deflate`, `br`, and `zstd` algorithms. +#[derive(Debug, Default)] +pub struct Decompression {} + +impl Decompression { + /// Creates a new Decompression middleware. + pub fn new() -> Self { + Self {} + } + + fn parse_encoding(s: &str) -> Result { + match s { + "gzip" => Ok(Encoding::Gzip), + "deflate" => Ok(Encoding::Deflate), + "br" => Ok(Encoding::Brotli), + "zstd" => Ok(Encoding::Zstd), + "identity" => Ok(Encoding::Identity), + _ => Err(Error::from(StatusCode::UNSUPPORTED_MEDIA_TYPE)), + } + } + + fn decode(&self, req: &mut Request) -> Result<(), Error> { + let encodings = if let Some(hval) = req.headers().get(CONTENT_ENCODING) { + let hval = match hval.to_str() { + Ok(hval) => hval, + Err(_) => return Err(Error::from(StatusCode::BAD_REQUEST)), + }; + hval.split(',') + .map(str::trim) + .rev() // apply decodings in reverse order + .map(Decompression::parse_encoding) + .collect::, Error>>()? + } else { + return Ok(()); + }; + + for encoding in encodings { + match encoding { + Encoding::Gzip => { + let body = std::mem::replace(req.body_mut(), Body::empty()); + let stream = stream::GzipDecoder::new(body); + *req.body_mut() = Body::from_stream(stream); + } + Encoding::Deflate => { + let body = std::mem::replace(req.body_mut(), Body::empty()); + let stream = stream::ZlibDecoder::new(body); + *req.body_mut() = Body::from_stream(stream); + } + Encoding::Brotli => { + let body = std::mem::replace(req.body_mut(), Body::empty()); + let stream = stream::BrotliDecoder::new(body); + *req.body_mut() = Body::from_stream(stream); + } + Encoding::Zstd => { + let body = std::mem::replace(req.body_mut(), Body::empty()); + let stream = stream::ZstdDecoder::new(body); + *req.body_mut() = Body::from_stream(stream); + } + Encoding::Identity => (), + } + } + + // strip the content-encoding header + req.headers_mut().remove(CONTENT_ENCODING).unwrap(); + + Ok(()) + } +} + +impl Middleware for Decompression { + fn handle<'a>( + &'a self, + mut cx: Context, + next: Next<'a, State>, + ) -> BoxFuture<'a, Response> { + FutureExt::boxed(async move { + match self.decode(cx.request_mut()) { + Ok(_) => (), + Err(e) => return e.into_response(), + }; + next.run(cx).await + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use async_compression::flate2; + use bytes::Bytes; + use futures::{ + executor::{block_on, block_on_stream}, + stream::StreamExt, + }; + use http::header::ACCEPT_ENCODING; + use http_service::Body; + use http_service_mock::make_server; + + async fn lorem_ipsum(_cx: Context<()>) -> String { + String::from(r#" + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam rutrum et risus sed egestas. Maecenas dapibus enim a posuere + semper. Cras venenatis et turpis quis aliquam. Suspendisse eget risus in libero tristique consectetur. Ut ut risus cursus, scelerisque + enim ac, tempus tellus. Vestibulum ac porta felis. Aenean fringilla posuere felis, in blandit enim tristique ut. Sed elementum iaculis + enim eu commodo. + "#) + } + + fn lorem_ipsum_bytes() -> Vec { + String::from(r#" + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam rutrum et risus sed egestas. Maecenas dapibus enim a posuere + semper. Cras venenatis et turpis quis aliquam. Suspendisse eget risus in libero tristique consectetur. Ut ut risus cursus, scelerisque + enim ac, tempus tellus. Vestibulum ac porta felis. Aenean fringilla posuere felis, in blandit enim tristique ut. Sed elementum iaculis + enim eu commodo. + "#).into_bytes() + } + + // Echoes the request body in bytes. + async fn echo_bytes(mut cx: Context<()>) -> Vec { + cx.body_bytes().await.unwrap() + } + + // Generates the app. + fn app() -> tide::App<()> { + let mut app = tide::App::new(); + app.at("/").get(lorem_ipsum); + app.at("/echo").post(echo_bytes); + app.middleware(Compression::new()); + app.middleware(Decompression::new()); + app + } + + // Generates a response given a string that represents the Accept-Encoding header value. + fn get_encoded_response(hval: &str) -> Response { + let app = app(); + let mut server = make_server(app.into_http_service()).unwrap(); + let req = http::Request::get("/") + .header(ACCEPT_ENCODING, hval) + .body(Body::empty()) + .unwrap(); + server.simulate(req).unwrap() + } + + // Generates a decoded response given a request body and the header value representing its encoding. + fn get_decoded_response(body: Body, hval: &str) -> Response { + let app = app(); + let mut server = make_server(app.into_http_service()).unwrap(); + let req = http::Request::post("/echo") + .header(CONTENT_ENCODING, hval) + .body(body) + .unwrap(); + server.simulate(req).unwrap() + } + + #[test] + fn compressed_gzip_response() { + let res = get_encoded_response("gzip"); + assert_eq!(res.status(), 200); + let body = res.into_body(); + let stream = stream::GzipDecoder::new(body); + let decompressed_body: Vec = block_on_stream(stream) + .map(Result::unwrap) + .flatten() + .collect(); + let lorem_ipsum = lorem_ipsum_bytes(); + assert_eq!(decompressed_body, lorem_ipsum); + } + + #[test] + fn compressed_deflate_response() { + let res = get_encoded_response("deflate"); + assert_eq!(res.status(), 200); + let body = res.into_body(); + let stream = stream::ZlibDecoder::new(body); + let decompressed_body: Vec = block_on_stream(stream) + .map(Result::unwrap) + .flatten() + .collect(); + let lorem_ipsum = lorem_ipsum_bytes(); + assert_eq!(decompressed_body, lorem_ipsum); + } + + #[test] + fn compressed_brotli_response() { + let res = get_encoded_response("br"); + assert_eq!(res.status(), 200); + let body = res.into_body(); + let stream = stream::BrotliDecoder::new(body); + let decompressed_body: Vec = block_on_stream(stream) + .map(Result::unwrap) + .flatten() + .collect(); + let lorem_ipsum = lorem_ipsum_bytes(); + assert_eq!(decompressed_body, lorem_ipsum); + } + + #[test] + fn compressed_zstd_response() { + let res = get_encoded_response("zstd"); + assert_eq!(res.status(), 200); + let body = res.into_body(); + let stream = stream::ZstdDecoder::new(body); + let decompressed_body: Vec = block_on_stream(stream) + .map(Result::unwrap) + .flatten() + .collect(); + let lorem_ipsum = lorem_ipsum_bytes(); + assert_eq!(decompressed_body, lorem_ipsum); + } + + #[test] + fn decompressed_gzip_response() { + let lorem_ipsum = lorem_ipsum_bytes(); + let req_body = Body::from_stream(stream::GzipEncoder::new( + futures::stream::iter(vec![lorem_ipsum]) + .map(Bytes::from) + .map(Ok), + flate2::Compression::default(), + )); + let res = get_decoded_response(req_body, "gzip"); + let body = block_on(res.into_body().into_vec()).unwrap(); + let lorem_ipsum = lorem_ipsum_bytes(); + assert_eq!(body, lorem_ipsum); + } + + #[test] + fn decompressed_deflate_response() { + let lorem_ipsum = lorem_ipsum_bytes(); + let req_body = Body::from_stream(stream::ZlibEncoder::new( + futures::stream::iter(vec![lorem_ipsum]) + .map(Bytes::from) + .map(Ok), + flate2::Compression::default(), + )); + let res = get_decoded_response(req_body, "deflate"); + let body = block_on(res.into_body().into_vec()).unwrap(); + let lorem_ipsum = lorem_ipsum_bytes(); + assert_eq!(body, lorem_ipsum); + } + + #[test] + fn decompressed_brotli_response() { + let lorem_ipsum = lorem_ipsum_bytes(); + let req_body = Body::from_stream(stream::BrotliEncoder::new( + futures::stream::iter(vec![lorem_ipsum]) + .map(Bytes::from) + .map(Ok), + 11, + )); + let res = get_decoded_response(req_body, "br"); + let body = block_on(res.into_body().into_vec()).unwrap(); + let lorem_ipsum = lorem_ipsum_bytes(); + assert_eq!(body, lorem_ipsum); + } + + #[test] + fn decompressed_zstd_response() { + let lorem_ipsum = lorem_ipsum_bytes(); + let req_body = Body::from_stream(stream::ZstdEncoder::new( + futures::stream::iter(vec![lorem_ipsum]) + .map(Bytes::from) + .map(Ok), + 3, + )); + let res = get_decoded_response(req_body, "zstd"); + let body = block_on(res.into_body().into_vec()).unwrap(); + let lorem_ipsum = lorem_ipsum_bytes(); + assert_eq!(body, lorem_ipsum); + } +} diff --git a/tide-cookies/Cargo.toml b/tide-cookies/Cargo.toml new file mode 100644 index 000000000..6c0742f4e --- /dev/null +++ b/tide-cookies/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tide-cookies" +version = "0.2.0" +edition = "2018" +authors = [ + "Tide Developers", +] +description = "Cookie middleware and extensions for Tide" +documentation = "https://docs.rs/tide-cookies" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rustasync/tide" + +[dependencies] +cookie = { version = "0.12", features = ["percent-encode"] } +futures-preview = "0.3.0-alpha.16" +http = "0.1" +http-service = "0.2.0" +tide-core = { path = "../tide-core" } + +[dev-dependencies] +tide = { path = "../" } +http-service-mock = "0.2.0" diff --git a/src/cookies.rs b/tide-cookies/src/data.rs similarity index 98% rename from src/cookies.rs rename to tide-cookies/src/data.rs index 48fa1a2cb..7430b10e0 100644 --- a/src/cookies.rs +++ b/tide-cookies/src/data.rs @@ -1,9 +1,8 @@ use cookie::{Cookie, CookieJar, ParseError}; -use crate::error::StringError; -use crate::Context; use http::HeaderMap; use std::sync::{Arc, RwLock}; +use tide_core::{error::StringError, Context}; const MIDDLEWARE_MISSING_MSG: &str = "CookiesMiddleware must be used to populate request and response cookies"; diff --git a/tide-cookies/src/lib.rs b/tide-cookies/src/lib.rs new file mode 100644 index 000000000..c1c72a651 --- /dev/null +++ b/tide-cookies/src/lib.rs @@ -0,0 +1,19 @@ +//! Crate that provides helpers and/or middlewares for Tide +//! related to cookies. + +#![feature(async_await)] +#![warn( + nonstandard_style, + rust_2018_idioms, + future_incompatible, + missing_debug_implementations, + missing_docs +)] + +//! Cookie management for Tide web framework + +mod data; +mod middleware; + +pub use self::data::ContextExt; +pub use self::middleware::CookiesMiddleware; diff --git a/src/middleware/cookies.rs b/tide-cookies/src/middleware.rs similarity index 86% rename from src/middleware/cookies.rs rename to tide-cookies/src/middleware.rs index 9c3dd6910..41c3e10db 100644 --- a/src/middleware/cookies.rs +++ b/tide-cookies/src/middleware.rs @@ -1,37 +1,42 @@ -use crate::cookies::CookieData; +use crate::data::CookieData; use futures::future::BoxFuture; +use futures::prelude::*; use http::header::HeaderValue; -use crate::{ +use tide_core::{ middleware::{Middleware, Next}, Context, Response, }; /// Middleware to work with cookies. /// -/// [`CookiesMiddleware`] along with [`ContextExt`](crate::cookies::ContextExt) provide smooth +/// [`CookiesMiddleware`] along with [`ContextExt`] provide smooth /// access to request cookies and setting/removing cookies from response. This leverages the /// [cookie](https://crates.io/crates/cookie) crate. /// This middleware parses cookies from request and caches them in the extension. Once the request /// is processed by endpoints and other middlewares, all the added and removed cookies are set on /// on the response. You will need to add this middle before any other middlewares that might need /// to access Cookies. +/// +/// [`CookiesMiddleware`]: crate::middleware::CookiesMiddleware +/// [`ContextExt`]: ../../tide/cookies/trait.ContextExt.html #[derive(Clone, Default, Debug)] pub struct CookiesMiddleware {} impl CookiesMiddleware { + /// CookieMiddleware constructor pub fn new() -> Self { Self {} } } -impl Middleware for CookiesMiddleware { +impl Middleware for CookiesMiddleware { fn handle<'a>( &'a self, - mut cx: Context, - next: Next<'a, Data>, + mut cx: Context, + next: Next<'a, State>, ) -> BoxFuture<'a, Response> { - box_async! { + FutureExt::boxed(async move { let cookie_data = cx .extensions_mut() .remove() @@ -56,46 +61,47 @@ impl Middleware for CookiesMiddleware { } } res - } + }) } } #[cfg(test)] mod tests { use super::*; - use crate::{cookies::ContextExt, Context}; + use crate::data::ContextExt; use cookie::Cookie; use futures::executor::block_on; use http_service::Body; use http_service_mock::make_server; + use tide_core::Context; static COOKIE_NAME: &str = "testCookie"; /// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter. - #[allow(unused_mut)] // Workaround clippy bug async fn retrieve_cookie(mut cx: Context<()>) -> String { - format!("{}", cx.get_cookie(COOKIE_NAME).unwrap().unwrap().value()) + cx.get_cookie(COOKIE_NAME) + .unwrap() + .unwrap() + .value() + .to_string() } - #[allow(unused_mut)] // Workaround clippy bug async fn set_cookie(mut cx: Context<()>) { cx.set_cookie(Cookie::new(COOKIE_NAME, "NewCookieValue")) .unwrap(); } - #[allow(unused_mut)] // Workaround clippy bug async fn remove_cookie(mut cx: Context<()>) { cx.remove_cookie(Cookie::named(COOKIE_NAME)).unwrap(); } - #[allow(unused_mut)] // Workaround clippy bug async fn set_multiple_cookie(mut cx: Context<()>) { cx.set_cookie(Cookie::new("C1", "V1")).unwrap(); cx.set_cookie(Cookie::new("C2", "V2")).unwrap(); } - fn app() -> crate::App<()> { - let mut app = crate::App::new(); + fn app() -> tide_core::App<()> { + let mut app = tide_core::App::new(); app.middleware(CookiesMiddleware::new()); app.at("/get").get(retrieve_cookie); @@ -112,8 +118,7 @@ mod tests { .header(http::header::COOKIE, "testCookie=RequestCookieValue") .body(Body::empty()) .unwrap(); - let res = server.simulate(req).unwrap(); - res + server.simulate(req).unwrap() } #[test] @@ -172,5 +177,4 @@ mod tests { assert!(iter.next().is_none()); } - } diff --git a/tide-core/Cargo.toml b/tide-core/Cargo.toml new file mode 100644 index 000000000..137b65f38 --- /dev/null +++ b/tide-core/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "tide-core" +version = "0.2.0" +edition = "2018" +authors = [ + "Tide Developers", +] +description = "Core types and traits for Tide web framework" +documentation = "https://docs.rs/tide-core" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rustasync/tide" + +[dependencies] +fnv = "1.0.6" +futures-preview = "0.3.0-alpha.16" +http = "0.1" +http-service = "0.2.0" +route-recognizer = "0.1.12" +serde = "1.0.91" +serde_json = "1.0.39" + +[dependencies.http-service-hyper] +optional = true +version = "0.2.0" + +[dev-dependencies] +tide = { path = "../" } +serde_derive = "1.0.91" + +[features] +default = ["hyper"] +hyper = ["http-service-hyper"] diff --git a/src/app.rs b/tide-core/src/app.rs similarity index 86% rename from src/app.rs rename to tide-core/src/app.rs index 7e4273036..b036fb0d0 100644 --- a/src/app.rs +++ b/tide-core/src/app.rs @@ -1,4 +1,5 @@ use futures::future::{self, BoxFuture}; +use futures::prelude::*; use http_service::HttpService; use std::sync::Arc; @@ -34,13 +35,13 @@ use crate::{ /// /// let mut app = tide::App::new(); /// app.at("/hello").get(async move |_| "Hello, world!"); -/// app.serve("127.0.0.1:8000"); +/// app.run("127.0.0.1:8000"); /// ``` /// /// # Routing and parameters /// /// 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 +/// 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: /// @@ -67,7 +68,7 @@ use crate::{ /// "Use /hello/{your name} or /goodbye/{your name}" /// }); /// -/// app.serve("127.0.0.1:8000"); +/// app.run("127.0.0.1:8000"); /// ``` /// /// You can learn more about routing in the [`App::at`] documentation. @@ -76,6 +77,7 @@ use crate::{ /// /// ```rust, no_run /// #![feature(async_await)] +/// #[macro_use] extern crate serde_derive; /// /// use http::status::StatusCode; /// use serde::{Deserialize, Serialize}; @@ -123,7 +125,7 @@ use crate::{ /// let mut app = App::with_state(Database::default()); /// app.at("/message").post(new_message); /// app.at("/message/:id").get(get_message); -/// app.serve("127.0.0.1:8000").unwrap(); +/// app.run("127.0.0.1:8000").unwrap(); /// } /// ``` @@ -131,7 +133,7 @@ use crate::{ pub struct App { router: Router, middleware: Vec>>, - data: State, + state: State, } impl App<()> { @@ -153,7 +155,7 @@ impl App { App { router: Router::new(), middleware: Vec::new(), - data: state, + state, } } @@ -181,12 +183,13 @@ impl App { /// 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. + /// Alternatively a wildcard definitions can start with a `*`, for example + /// `*path`, which means that the wildcard will match to the end of given + /// path, no matter how many segments are left, even nothing. + /// + /// The name of the parameter can be omitted to define a path that matches + /// the required structure, but where the parameters are not required. + /// `:` will match a segment, and `*` will match an entire path. /// /// Here are some examples omitting the HTTP verb based endpoint selection: /// @@ -195,7 +198,9 @@ impl App { /// app.at("/"); /// app.at("/hello"); /// app.at("add_two/:num"); - /// app.at("static/:path*"); + /// app.at("files/:user/*"); + /// app.at("static/*path"); + /// app.at("static/:context/:"); /// ``` /// /// There is no fallback route matching, i.e. either a resource is a full @@ -215,6 +220,8 @@ impl App { /// /// Middleware can only be added at the "top level" of an application, /// and is processed in the order in which it is applied. + /// + /// [`Middleware`]: crate::middleware::Middleware pub fn middleware(&mut self, m: impl Middleware) -> &mut Self { self.middleware.push(Arc::new(m)); self @@ -227,16 +234,16 @@ impl App { pub fn into_http_service(self) -> Server { Server { router: Arc::new(self.router), - data: Arc::new(self.data), + state: Arc::new(self.state), middleware: Arc::new(self.middleware), } } - /// Start serving the app at the given address. + /// Run the app at the given address. /// /// Blocks the calling thread indefinitely. #[cfg(feature = "hyper")] - pub fn serve(self, addr: impl std::net::ToSocketAddrs) -> std::io::Result<()> { + pub fn run(self, addr: impl std::net::ToSocketAddrs) -> std::io::Result<()> { let addr = addr .to_socket_addrs()? .next() @@ -246,17 +253,32 @@ impl App { http_service_hyper::run(self.into_http_service(), addr); Ok(()) } + + /// Asynchronously serve the app at the given address. + #[cfg(feature = "hyper")] + pub async fn serve(self, addr: impl std::net::ToSocketAddrs) -> std::io::Result<()> { + let addr = addr + .to_socket_addrs()? + .next() + .ok_or(std::io::ErrorKind::InvalidInput)?; + + http_service_hyper::serve(self.into_http_service(), addr) + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + } } /// 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. +/// +/// [`HttpService`]: http_service::HttpService #[derive(Clone)] #[allow(missing_debug_implementations)] pub struct Server { router: Arc>, - data: Arc, + state: Arc, middleware: Arc>>>, } @@ -274,12 +296,12 @@ impl HttpService for Server { let method = req.method().to_owned(); let router = self.router.clone(); let middleware = self.middleware.clone(); - let data = self.data.clone(); + let state = self.state.clone(); - box_async! { + FutureExt::boxed(async move { let fut = { let Selection { endpoint, params } = router.route(&path, method); - let cx = Context::new(data, req, params); + let cx = Context::new(state, req, params); let next = Next { endpoint, @@ -290,7 +312,7 @@ impl HttpService for Server { }; Ok(fut.await) - } + }) } } @@ -302,19 +324,19 @@ mod tests { use super::*; use crate::{middleware::Next, router::Selection, Context, Response}; - fn simulate_request<'a, Data: Default + Clone + Send + Sync + 'static>( - app: &'a App, + fn simulate_request<'a, State: Default + Clone + Send + Sync + 'static>( + app: &'a App, path: &'a str, method: http::Method, ) -> BoxFuture<'a, Response> { let Selection { endpoint, params } = app.router.route(path, method.clone()); - let data = Arc::new(Data::default()); + let state = Arc::new(State::default()); let req = http::Request::builder() .method(method) .body(http_service::Body::empty()) .unwrap(); - let cx = Context::new(data, req, params); + let cx = Context::new(state, req, params); let next = Next { endpoint, next_middleware: &app.middleware, diff --git a/src/context.rs b/tide-core/src/context.rs similarity index 94% rename from src/context.rs rename to tide-core/src/context.rs index 4c96cc896..d3e1bacd5 100644 --- a/src/context.rs +++ b/tide-core/src/context.rs @@ -3,7 +3,7 @@ use http_service::Body; use route_recognizer::Params; use std::{str::FromStr, sync::Arc}; -/// Data associated with a request-response lifecycle. +/// State 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. @@ -55,7 +55,12 @@ impl Context { &self.request } - /// Access app-global data. + /// Access a mutable handle to the entire request. + pub fn request_mut(&mut self) -> &mut http_service::Request { + &mut self.request + } + + /// Access the state. pub fn state(&self) -> &State { &self.state } diff --git a/src/endpoint.rs b/tide-core/src/endpoint.rs similarity index 89% rename from src/endpoint.rs rename to tide-core/src/endpoint.rs index f4adbfcf8..2b379295c 100644 --- a/src/endpoint.rs +++ b/tide-core/src/endpoint.rs @@ -1,4 +1,5 @@ use futures::future::{BoxFuture, Future}; +use futures::prelude::*; use crate::{response::IntoResponse, Context, Response}; @@ -8,7 +9,7 @@ use crate::{response::IntoResponse, Context, Response}; /// directly by Tide users. /// /// In practice, endpoints are functions that take a `Context` as an argument and -/// return a type `T` that implements [`IntoResponse`]. +/// return a type `T` that implements [`IntoResponse`](crate::response::IntoResponse). /// /// # Examples /// @@ -28,7 +29,7 @@ use crate::{response::IntoResponse, Context, Response}; /// fn main() { /// let mut app = tide::App::new(); /// app.at("/hello").get(hello); -/// app.serve("127.0.0.1:8000").unwrap() +/// app.run("127.0.0.1:8000").unwrap() /// } /// ``` /// @@ -43,7 +44,7 @@ use crate::{response::IntoResponse, Context, Response}; /// fn main() { /// let mut app = tide::App::new(); /// app.at("/hello").get(hello); -/// app.serve("127.0.0.1:8000").unwrap() +/// app.run("127.0.0.1:8000").unwrap() /// } /// ``` /// @@ -68,8 +69,6 @@ where type Fut = BoxFuture<'static, Response>; fn call(&self, cx: Context) -> Self::Fut { let fut = (self)(cx); - box_async! { - fut.await.into_response() - } + FutureExt::boxed(async move { fut.await.into_response() }) } } diff --git a/src/error.rs b/tide-core/src/error.rs similarity index 66% rename from src/error.rs rename to tide-core/src/error.rs index 174c6987a..5062939ca 100644 --- a/src/error.rs +++ b/tide-core/src/error.rs @@ -1,16 +1,11 @@ -use core::pin::Pin; -use futures::future::Future; -use http::{HttpTryFrom, Response, StatusCode}; -use http_service::Body; +//! Error and Result module use crate::response::IntoResponse; - -pub(crate) type BoxTryFuture = Pin> + Send + 'static>>; - -/// A convenient `Result` instantiation appropriate for most endpoints. -pub type EndpointResult> = Result; +use http::{HttpTryFrom, Response, StatusCode}; +use http_service::Body; #[derive(Debug)] +/// A string error, which can be display pub struct StringError(pub String); impl std::error::Error for StringError {} @@ -20,12 +15,17 @@ impl std::fmt::Display for StringError { } } +#[macro_export] +/// Macro that generates StringError immediately macro_rules! err_fmt { {$($t:tt)*} => { - crate::error::StringError(format!($($t)*)) + $crate::error::StringError(format!($($t)*)) } } +/// A convenient `Result` instantiation appropriate for most endpoints. +pub type EndpointResult> = Result; + /// A generic endpoint error, which can be converted into a response. #[derive(Debug)] pub struct Error { @@ -56,6 +56,18 @@ impl From for Error { } } +/// Extends the `Response` type with a method to extract error causes when applicable. +pub trait ResponseExt { + /// Extract the cause of the unsuccessful response, if any + fn err_cause(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)>; +} + +impl ResponseExt for Response { + fn err_cause(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> { + self.extensions().get().map(|Cause(c)| &**c) + } +} + /// 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 @@ -70,26 +82,44 @@ pub trait ResultExt: Sized { self.with_err_status(500) } - /// Convert to an `EndpointResult`, wrapping the `Err` case with a custom - /// response status. + /// Convert to an `EndpointResult`, wrapping the `Err` case with a custom response status. fn with_err_status(self, status: S) -> EndpointResult where StatusCode: HttpTryFrom; } -/// Extends the `Response` type with a method to extract error causes when applicable. -pub trait ResponseExt { - /// Extract the cause of the unsuccessful response, if any - fn err_cause(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)>; +impl ResultExt for std::result::Result { + fn with_err_status(self, status: S) -> EndpointResult + where + StatusCode: HttpTryFrom, + { + let r = self.map_err(|e| Box::new(e) as Box); + r.with_err_status(status) + } } -impl ResponseExt for Response { - fn err_cause(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> { - self.extensions().get().map(|Cause(c)| &**c) +/// Extends the `Result` type using `std::error::Error` trait object as the error type with +/// convenient methods for constructing Tide errors. +pub trait ResultDynErrExt: 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 { +impl ResultDynErrExt for std::result::Result> { fn with_err_status(self, status: S) -> EndpointResult where StatusCode: HttpTryFrom, @@ -97,7 +127,7 @@ impl ResultExt for std::resu self.map_err(|e| Error { resp: Response::builder() .status(status) - .extension(Cause(Box::new(e))) + .extension(Cause(e)) .body(Body::empty()) .unwrap(), }) diff --git a/tide-core/src/internal.rs b/tide-core/src/internal.rs new file mode 100644 index 000000000..9a2ad59f1 --- /dev/null +++ b/tide-core/src/internal.rs @@ -0,0 +1,9 @@ +//! For internal use. These APIs will never be stable and +//! are meant to be used internally by the tide repo. + +use core::pin::Pin; +use futures::future::Future; + +/// Convenience alias for pinned box of Future> + Send + 'static +pub type BoxTryFuture = + Pin> + Send + 'static>>; diff --git a/tide-core/src/lib.rs b/tide-core/src/lib.rs new file mode 100644 index 000000000..4ede67a03 --- /dev/null +++ b/tide-core/src/lib.rs @@ -0,0 +1,40 @@ +//! Core types and traits from Tide + +#![feature(async_await, existential_type)] +#![warn( + nonstandard_style, + rust_2018_idioms, + future_incompatible, + missing_debug_implementations, + missing_docs +)] +// TODO: Remove this after clippy bug due to async await is resolved. +// ISSUE: https://github.com/rust-lang/rust-clippy/issues/3988 +#![allow(clippy::needless_lifetimes)] + +//! +//! Tide core api document +//! +//! The [`App`] docs are a good place to get started. +//! + +mod app; +mod context; +mod endpoint; +pub mod error; +pub mod middleware; +pub mod response; +mod route; +mod router; + +// Internal shared API for limited use across crates in our repo +pub mod internal; + +pub use crate::{ + app::{App, Server}, + context::Context, + endpoint::Endpoint, + error::{EndpointResult, Error}, + response::Response, + route::Route, +}; diff --git a/src/middleware/mod.rs b/tide-core/src/middleware.rs similarity index 74% rename from src/middleware/mod.rs rename to tide-core/src/middleware.rs index 64c6c5692..12129edd0 100644 --- a/src/middleware/mod.rs +++ b/tide-core/src/middleware.rs @@ -1,13 +1,8 @@ -use futures::future::BoxFuture; -use std::sync::Arc; +//! Middlewares use crate::{endpoint::DynEndpoint, Context, Response}; - -mod cookies; -mod default_headers; -mod logger; - -pub use self::{cookies::CookiesMiddleware, default_headers::DefaultHeaders, logger::RootLogger}; +use futures::future::BoxFuture; +use std::sync::Arc; /// Middleware that wraps around remaining middleware chain. pub trait Middleware: 'static + Send + Sync { @@ -15,11 +10,14 @@ pub trait Middleware: 'static + Send + Sync { fn handle<'a>(&'a self, cx: Context, next: Next<'a, State>) -> BoxFuture<'a, Response>; } -impl Middleware for F +impl Middleware for F where - F: Send + Sync + 'static + for<'a> Fn(Context, Next<'a, Data>) -> BoxFuture<'a, Response>, + F: Send + + Sync + + 'static + + for<'a> Fn(Context, Next<'a, State>) -> BoxFuture<'a, Response>, { - fn handle<'a>(&'a self, cx: Context, next: Next<'a, Data>) -> BoxFuture<'a, Response> { + fn handle<'a>(&'a self, cx: Context, next: Next<'a, State>) -> BoxFuture<'a, Response> { (self)(cx, next) } } diff --git a/src/response.rs b/tide-core/src/response.rs similarity index 98% rename from src/response.rs rename to tide-core/src/response.rs index 90751b25d..0adea7e63 100644 --- a/src/response.rs +++ b/tide-core/src/response.rs @@ -1,5 +1,8 @@ +//! Multiple types of response modules + use http_service::Body; +/// An Http response pub type Response = http_service::Response; /// Serialize `t` into a JSON-encoded response. diff --git a/src/route.rs b/tide-core/src/route.rs similarity index 98% rename from src/route.rs rename to tide-core/src/route.rs index 0406e793a..dc5297073 100644 --- a/src/route.rs +++ b/tide-core/src/route.rs @@ -37,6 +37,7 @@ impl<'a, State: 'static> Route<'a, State> { } } + /// Add endpoint nested routes pub fn nest(&mut self, f: impl FnOnce(&mut Route<'a, State>)) -> &mut Self { f(self); self diff --git a/src/router.rs b/tide-core/src/router.rs similarity index 85% rename from src/router.rs rename to tide-core/src/router.rs index 34710110d..1ce768dad 100644 --- a/src/router.rs +++ b/tide-core/src/router.rs @@ -1,5 +1,6 @@ use fnv::FnvHashMap; -use futures::future::{BoxFuture, FutureExt}; +use futures::future::BoxFuture; +use futures::prelude::*; use http_service::Body; use route_recognizer::{Match, Params, Router as MethodRouter}; @@ -61,8 +62,11 @@ impl Router { } } -fn not_found_endpoint(_cx: Context) -> BoxFuture<'static, Response> { - box_async! { - http::Response::builder().status(http::StatusCode::NOT_FOUND).body(Body::empty()).unwrap() - } +fn not_found_endpoint(_cx: Context) -> BoxFuture<'static, Response> { + FutureExt::boxed(async move { + http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .body(Body::empty()) + .unwrap() + }) } diff --git a/tide-forms/Cargo.toml b/tide-forms/Cargo.toml new file mode 100644 index 000000000..3b5fe4a44 --- /dev/null +++ b/tide-forms/Cargo.toml @@ -0,0 +1,31 @@ +[package] +authors = [ + "Tide Developers" +] +description = "Form helpers and extensions for Tide" +documentation = "https://docs.rs/tide-forms" +keywords = ["tide", "web", "async", "helpers", "forms"] +categories = [ + "network-programming", + "web-programming::http-server", +] +edition = "2018" +license = "MIT OR Apache-2.0" +name = "tide-forms" +readme = "README.md" +repository = "https://github.com/rustasync/tide" +version = "0.1.0" + +[dependencies] +tide-core = { path = "../tide-core" } +http-service = "0.2.0" +futures-preview = "0.3.0-alpha.16" +http = "0.1" +log = "0.4.6" +multipart = { version = "0.16.1", features = ["server"], default-features = false } +serde = { version = "1.0.91", features = ["derive"] } +serde_urlencoded = "0.5.5" + +[dev-dependencies] +tide = { path = "../" } + diff --git a/src/forms.rs b/tide-forms/src/lib.rs similarity index 65% rename from src/forms.rs rename to tide-forms/src/lib.rs index b7338dab5..67165c2e6 100644 --- a/src/forms.rs +++ b/tide-forms/src/lib.rs @@ -1,14 +1,23 @@ +//! Crate that provides helpers and extensions for Tide +//! related to forms. + +#![feature(async_await)] +#![warn( + nonstandard_style, + rust_2018_idioms, + future_incompatible, + missing_debug_implementations +)] + +use futures::prelude::*; use http_service::Body; use multipart::server::Multipart; use std::io::Cursor; -use crate::{ - error::{BoxTryFuture, ResultExt}, - Context, Response, -}; +use tide_core::{err_fmt, error::ResultExt, internal::BoxTryFuture, Context, Response}; /// An extension trait for `Context`, providing form extraction. -pub trait ExtractForms { +pub trait ContextExt { /// Asynchronously extract the entire body as a single form. fn body_form(&mut self) -> BoxTryFuture; @@ -16,13 +25,15 @@ pub trait ExtractForms { fn body_multipart(&mut self) -> BoxTryFuture>>>; } -impl ExtractForms for Context { +impl ContextExt for Context { fn body_form(&mut self) -> BoxTryFuture { let body = self.take_body(); - box_async! { + FutureExt::boxed(async move { let body = body.into_vec().await.client_err()?; - Ok(serde_urlencoded::from_bytes(&body).map_err(|e| err_fmt!("could not decode form: {}", e)).client_err()?) - } + Ok(serde_urlencoded::from_bytes(&body) + .map_err(|e| err_fmt!("could not decode form: {}", e)) + .client_err()?) + }) } fn body_multipart(&mut self) -> BoxTryFuture>>> { @@ -35,11 +46,13 @@ impl ExtractForms for Context { let body = self.take_body(); - box_async! { + FutureExt::boxed(async move { let body = body.into_vec().await.client_err()?; - let boundary = boundary.ok_or_else(|| err_fmt!("no boundary found")).client_err()?; + let boundary = boundary + .ok_or_else(|| err_fmt!("no boundary found")) + .client_err()?; Ok(Multipart::with_body(Cursor::new(body), boundary)) - } + }) } } diff --git a/tide-headers/Cargo.toml b/tide-headers/Cargo.toml new file mode 100644 index 000000000..294bc7791 --- /dev/null +++ b/tide-headers/Cargo.toml @@ -0,0 +1,26 @@ +[package] +authors = [ + "Tide Developers" +] +description = "Header related middleware for Tide" +documentation = "https://docs.rs/tide-headers" +keywords = ["tide", "web", "async", "middleware", "headers"] +categories = [ + "network-programming", + "web-programming::http-server", +] +edition = "2018" +license = "MIT OR Apache-2.0" +name = "tide-headers" +readme = "README.md" +repository = "https://github.com/rustasync/tide" +version = "0.1.0" + + [dependencies] +tide-core = { path = "../tide-core" } +futures-preview = "0.3.0-alpha.16" +http = "0.1" +log = "0.4.6" + +[dev-dependencies] +tide = { path = "../" } diff --git a/src/middleware/default_headers.rs b/tide-headers/src/lib.rs similarity index 65% rename from src/middleware/default_headers.rs rename to tide-headers/src/lib.rs index d7b5bffe4..3795a3f97 100644 --- a/src/middleware/default_headers.rs +++ b/tide-headers/src/lib.rs @@ -1,11 +1,24 @@ +//! Crate that provides helpers and/or middlewares for Tide +//! related to http headers. + +#![feature(async_await)] +#![warn( + nonstandard_style, + rust_2018_idioms, + future_incompatible, + missing_debug_implementations +)] + use futures::future::BoxFuture; +use futures::prelude::*; +use log::trace; use http::{ header::{HeaderValue, IntoHeaderName}, HeaderMap, HttpTryFrom, }; -use crate::{ +use tide_core::{ middleware::{Middleware, Next}, Context, Response, }; @@ -19,10 +32,9 @@ pub struct DefaultHeaders { impl DefaultHeaders { /// Construct a new instance with an empty list of headers. pub fn new() -> DefaultHeaders { - DefaultHeaders::default() + Self::default() } - #[inline] /// Add a header to the default header list. pub fn header(mut self, key: K, value: V) -> Self where @@ -34,21 +46,20 @@ impl DefaultHeaders { .expect("Cannot create default header"); self.headers.append(key, value); - self } } -impl Middleware for DefaultHeaders { - fn handle<'a>(&'a self, cx: Context, next: Next<'a, Data>) -> BoxFuture<'a, Response> { - box_async! { +impl Middleware for DefaultHeaders { + fn handle<'a>(&'a self, cx: Context, next: Next<'a, State>) -> BoxFuture<'a, Response> { + FutureExt::boxed(async move { let mut res = next.run(cx).await; - let headers = res.headers_mut(); for (key, value) in self.headers.iter() { + trace!("add default: {} {:?}", &key, &value); headers.entry(key).unwrap().or_insert_with(|| value.clone()); } res - } + }) } } diff --git a/tide-log/Cargo.toml b/tide-log/Cargo.toml new file mode 100644 index 000000000..4a2074b85 --- /dev/null +++ b/tide-log/Cargo.toml @@ -0,0 +1,27 @@ +[package] +authors = [ + "Tide Developers" +] +description = "Logging middleware for Tide" +documentation = "https://docs.rs/tide-log" +keywords = ["tide", "web", "async", "middleware", "logging"] +categories = [ + "logging", + "network-programming", + "web-programming::http-server", +] +edition = "2018" +license = "MIT OR Apache-2.0" +name = "tide-log" +readme = "README.md" +repository = "https://github.com/rustasync/tide" +version = "0.1.0" + + [dependencies] +tide-core = { path = "../tide-core" } +futures-preview = "0.3.0-alpha.16" +http = "0.1" +log = "0.4.6" + +[dev-dependencies] +tide = { path = "../" } diff --git a/tide-log/src/lib.rs b/tide-log/src/lib.rs new file mode 100644 index 000000000..496d58fb1 --- /dev/null +++ b/tide-log/src/lib.rs @@ -0,0 +1,77 @@ +//! Crate that provides helpers and/or middlewares for Tide +//! related to logging. + +#![feature(async_await)] +#![warn( + nonstandard_style, + rust_2018_idioms, + future_incompatible, + missing_debug_implementations +)] + +use futures::future::BoxFuture; +use futures::prelude::*; +use log::{info, trace}; + +use tide_core::{ + middleware::{Middleware, Next}, + Context, Response, +}; + +/// A simple requests logger +/// +/// # Examples +/// +/// ```rust +/// +/// let mut app = tide::App::new(); +/// app.middleware(tide_log::RequestLogger::new()); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct RequestLogger { + target: String, +} + +impl RequestLogger { + /// Create a new instance of logger with default target as + /// "requests" + pub fn new() -> Self { + Self { + target: "requests".to_owned(), + } + } + + /// Create a new instance of logger with supplied `target` for + /// logging. + pub fn with_target(target: String) -> Self { + Self { target } + } + + async fn log_basic<'a, State: Send + Sync + 'static>( + &'a self, + ctx: Context, + next: Next<'a, State>, + ) -> Response { + let path = ctx.uri().path().to_owned(); + let method = ctx.method().as_str().to_owned(); + trace!(target: &self.target, "IN => {} {}", method, path); + let start = std::time::Instant::now(); + let res = next.run(ctx).await; + let status = res.status(); + info!( + target: &self.target, + "{} {} {} {}ms", + method, + path, + status.as_str(), + start.elapsed().as_millis() + ); + res + } +} + +impl Middleware for RequestLogger { + fn handle<'a>(&'a self, ctx: Context, next: Next<'a, State>) -> BoxFuture<'a, Response> { + FutureExt::boxed(async move { self.log_basic(ctx, next).await }) + } +} diff --git a/tide-querystring/Cargo.toml b/tide-querystring/Cargo.toml new file mode 100644 index 000000000..5f5bbbd95 --- /dev/null +++ b/tide-querystring/Cargo.toml @@ -0,0 +1,31 @@ +[package] +authors = [ + "Tide Developers" +] +description = "Query string helpers and extensions for Tide" +documentation = "https://docs.rs/tide-querystring" +keywords = ["tide", "web", "async", "helpers", "querystring"] +categories = [ + "network-programming", + "web-programming::http-server", +] +edition = "2018" +license = "MIT OR Apache-2.0" +name = "tide-querystring" +readme = "README.md" +repository = "https://github.com/rustasync/tide" +version = "0.1.0" + + [dependencies] +tide-core = { path = "../tide-core" } +futures-preview = "0.3.0-alpha.16" +http = "0.1" +log = "0.4.6" +serde = { version = "1.0.91", features = ["derive"] } +serde_urlencoded = "0.5.5" + +[dev-dependencies] +tide = { path = "../" } +http-service = "0.2.0" +http-service-mock = "0.2.0" + diff --git a/src/querystring.rs b/tide-querystring/src/lib.rs similarity index 79% rename from src/querystring.rs rename to tide-querystring/src/lib.rs index 883108a32..15e735a7f 100644 --- a/src/querystring.rs +++ b/tide-querystring/src/lib.rs @@ -1,21 +1,31 @@ -use crate::{error::Error, Context}; +//! Crate that provides helpers and extensions for Tide +//! related to query strings. + +#![feature(async_await)] +#![warn( + nonstandard_style, + rust_2018_idioms, + future_incompatible, + missing_debug_implementations +)] + use http::StatusCode; use serde::Deserialize; +use tide_core::{error::Error, Context}; /// An extension trait for `Context`, providing query string deserialization. pub trait ContextExt<'de> { + /// Analyze url and extract query parameters fn url_query>(&'de self) -> Result; } -impl<'de, Data> ContextExt<'de> for Context { +impl<'de, State> ContextExt<'de> for Context { #[inline] fn url_query>(&'de self) -> Result { let query = self.uri().query(); - if query.is_none() { return Err(Error::from(StatusCode::BAD_REQUEST)); } - Ok(serde_urlencoded::from_str(query.unwrap()) .map_err(|_| Error::from(StatusCode::BAD_REQUEST))?) } @@ -27,20 +37,20 @@ mod tests { use futures::executor::block_on; use http_service::Body; use http_service_mock::make_server; - use serde_derive::Deserialize; + use serde::Deserialize; #[derive(Deserialize)] struct Params { msg: String, } - async fn handler(cx: crate::Context<()>) -> Result { + async fn handler(cx: tide::Context<()>) -> Result { let p = cx.url_query::()?; Ok(p.msg) } - fn app() -> crate::App<()> { - let mut app = crate::App::new(); + fn app() -> tide::App<()> { + let mut app = tide::App::new(); app.at("/").get(handler); app } diff --git a/tide-sessions/Cargo.toml b/tide-sessions/Cargo.toml new file mode 100644 index 000000000..254d588df --- /dev/null +++ b/tide-sessions/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "tide-sessions" +version = "0.0.1" +edition = "2018" +authors = [ + "Tide Developers", +] +description = "Session middleware and extensions for Tide" +documentation = "https://docs.rs/tide-sessions" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rustasync/tide" + +[dependencies] +tide = { path = "../" } +cookie = { version = "0.12", features = ["percent-encode"] } +futures-preview = "0.3.0-alpha.16" +http = "0.1" +http-service = "0.2.0" +tide-core = { path = "../tide-core" } + +[dev-dependencies] +tide = { path = "../" } +http-service-mock = "0.2.0" diff --git a/tide-sessions/src/ext.rs b/tide-sessions/src/ext.rs new file mode 100644 index 000000000..d426156e1 --- /dev/null +++ b/tide-sessions/src/ext.rs @@ -0,0 +1,27 @@ +use crate::session_cell::SessionCell; +use crate::session_map::SessionMap; +use std::cell::{ Ref, RefMut }; +use std::sync::Arc; +use tide::Context; + +// If a handler needs access to the session (mutably or immutably) it can +// import this trait. +pub trait SessionExt { + fn session(&self) -> Ref>; + fn session_mut(&self) -> RefMut>; +} + +impl< + Data: Clone + Send + Sync + 'static +> SessionExt for Context { + fn session(&self) -> Ref> { + let session_cell = self.extensions().get::>().unwrap(); + session_cell.0.borrow() + } + + fn session_mut(&self) -> RefMut> { + let session_cell = self.extensions().get::>().unwrap(); + session_cell.0.borrow_mut() + } +} + diff --git a/tide-sessions/src/lib.rs b/tide-sessions/src/lib.rs new file mode 100644 index 000000000..4ca6f4cdc --- /dev/null +++ b/tide-sessions/src/lib.rs @@ -0,0 +1,163 @@ +#![feature(async_await)] +use std::cell::Ref; +use tide::{ + Context, + Response, + cookies::ContextExt, + middleware::{ Middleware, Next } +}; +use futures::future::BoxFuture; +use futures::prelude::*; +use http::header::HeaderValue; +use cookie::{ Cookie, CookieBuilder }; + +mod session_cell; +mod session_map; +mod ext; + +pub use crate::session_map::SessionMap; +pub use crate::ext::SessionExt; + +use self::session_cell::SessionCell; + +pub trait SessionStore { + fn load_session(&self, key: &str) -> SessionMap; + fn create_session(&self) -> SessionMap { + SessionMap::new() + } + fn commit(&self, key: Option<&str>, session: Ref>) -> Result; +} + +pub struct SessionMiddleware CookieBuilder + 'static> { + pub session_key: String, + pub store: Store, + + pub configure_session_cookie: Configure +} + +impl< + Data: Clone + Send + Sync + 'static, + S: SessionStore + Send + Sync + 'static, + C: Send + Sync + Fn(CookieBuilder) -> CookieBuilder + 'static +> Middleware for SessionMiddleware { + fn handle<'a>(&'a self, mut ctx: Context, next: Next<'a, Data>) -> BoxFuture<'a, Response> { + + FutureExt::boxed(async move { + let result_maybe_session = ctx.get_cookie(&self.session_key); + + let session_key = match result_maybe_session { + Ok(maybe_session) => match maybe_session { + Some(cookie) => Some(String::from(cookie.value())), + None => None + }, + Err(_) => None + }; + + let session = match session_key.as_ref() { + Some(value) => { + self.store.load_session(&value) + }, + None => self.store.create_session() + }; + + // Create a ref-counted cell. Attach a clone of that ARC'd cell to + // the context and send it through. Meanwhile, keep our local copy + // of the arc ready for inspection after we're done processing the + // request. + let cell = SessionCell::new(session); + ctx.extensions_mut().insert(cell.clone()); + let mut res = next.run(ctx).await; + + // Borrow the session map and check to see if we need to commit + // it and/or send a new cookie. + let session_cell = &cell.0; + let session = session_cell.borrow(); + if !SessionMap::dirty(&session) { + return res + } + + if let Ok(sid) = self.store.commit(session_key.as_ref().map(String::as_str), session) { + if session_key.is_none() { + let builder = Cookie::build(self.session_key.clone(), sid); + let c = (self.configure_session_cookie)( + builder + ).finish(); + + if let Ok(value) = HeaderValue::from_str(&c.to_string()) { + // TODO: is there a good way to play nicely with cookie + // middleware? Can we rely on additions to context that + // other middleware add during the response half of the + // request lifecycle? + + let headers = res.headers_mut(); + headers.insert("Set-Cookie", value); + } + } + } + + res + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::{ + executor::{block_on, block_on_stream}, + stream::StreamExt, + }; + use http_service::Body; + use http_service_mock::make_server; + use cookie::CookieBuilder; + + // Generates the app. + fn app(mw: SessionMiddleware) -> tide::App<()> where + S: SessionStore + Send + Sync + 'static, + C: Fn(CookieBuilder) -> CookieBuilder + Send + Sync { + let mut app = tide::App::new(); + app.at("/session/:key").get(async move |ctx: Context<()>| { + let session = ctx.session(); + let key: String = ctx.param("key").expect("failed to parse url param"); + session.get(&key).expect("expected to be able to read key").clone() + }).post(async move |mut ctx: Context<()>| { + let key: String = ctx.param("key").expect("failed to parse url param"); + let body = ctx.body_string().await.expect("failed to read test request body"); + let mut session = ctx.session_mut(); + session.insert(key, body); + "ok" + }); + app.middleware(mw); + app + } + + fn configure_cookie (builder: CookieBuilder) -> CookieBuilder { + builder + } + + struct InMemorySessionStore; + impl SessionStore for InMemorySessionStore { + fn load_session(&self, key: &str) -> SessionMap { + SessionMap::new() + } + + fn commit(&self, key: Option<&str>, session: Ref>) -> Result { + Ok(String::from("hi")) + } + } + + + #[test] + fn set_cookie_creates_new_session_id() { + let app = app(SessionMiddleware { + session_key: String::from("sid"), + store: InMemorySessionStore { }, + configure_session_cookie: configure_cookie + }); + let mut server = make_server(app.into_http_service()).unwrap(); + let req = http::Request::post("/session/testkey") + .body(Body::from("hello world")) + .unwrap(); + server.simulate(req).unwrap(); + } +} diff --git a/tide-sessions/src/session_cell.rs b/tide-sessions/src/session_cell.rs new file mode 100644 index 000000000..e4dc8af1a --- /dev/null +++ b/tide-sessions/src/session_cell.rs @@ -0,0 +1,19 @@ +use crate::session_map::SessionMap; +use std::cell::RefCell; +use std::sync::Arc; + +#[derive(Clone)] +pub struct SessionCell(pub RefCell>); + +// We're copying actix, here. I need to understand this better, because +// this strikes me as dangerous. +#[doc(hidden)] +unsafe impl Send for SessionCell {} +#[doc(hidden)] +unsafe impl Sync for SessionCell {} + +impl SessionCell { + pub fn new (map: SessionMap) -> Arc { + Arc::new(Self(RefCell::new(Box::new(map)))) + } +} diff --git a/tide-sessions/src/session_map.rs b/tide-sessions/src/session_map.rs new file mode 100644 index 000000000..298670ab2 --- /dev/null +++ b/tide-sessions/src/session_map.rs @@ -0,0 +1,45 @@ +use std::{ + cell::Ref, + ops::{ Deref, DerefMut } +}; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct SessionMap { + is_dirty: bool, + data: HashMap // XXX: this could be made more generic +} + +// Provide associated functions a la Box or Arc, so we can +// Deref directly to the internal HashMap. +impl SessionMap { + pub fn dirty(target: &Ref>) -> bool { + target.is_dirty + } + + pub fn rotate(target: &mut Self) { + target.is_dirty = true + } + + pub fn new() -> Self { + Self { + is_dirty: false, + data: HashMap::new() + } + } +} + +impl Deref for SessionMap { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DerefMut for SessionMap { + fn deref_mut(&mut self) -> &mut Self::Target { + SessionMap::rotate(self); + &mut self.data + } +} diff --git a/tide-slog/Cargo.toml b/tide-slog/Cargo.toml new file mode 100644 index 000000000..dd00a8b04 --- /dev/null +++ b/tide-slog/Cargo.toml @@ -0,0 +1,30 @@ +[package] +authors = [ + "Tide Developers" +] +description = "Logging middleware for Tide based on slog" +documentation = "https://docs.rs/tide-slog" +keywords = ["tide", "web", "middleware", "logging", "slog"] +categories = [ + "logging", + "network-programming", + "web-programming::http-server", +] +edition = "2018" +license = "MIT OR Apache-2.0" +name = "tide-slog" +readme = "README.md" +repository = "https://github.com/rustasync/tide" +version = "0.1.0" + + [dependencies] +tide-core = { path = "../tide-core" } +futures-preview = "0.3.0-alpha.16" +http = "0.1" +log = "0.4.6" +slog = "2.4.1" +slog-async = "2.3.0" +slog-term = "2.4.0" + +[dev-dependencies] +tide = { path = "../" } diff --git a/tide-slog/src/lib.rs b/tide-slog/src/lib.rs new file mode 100644 index 000000000..4df5ad6bb --- /dev/null +++ b/tide-slog/src/lib.rs @@ -0,0 +1,74 @@ +//! Crate that provides helpers and/or middlewares for Tide +//! related to structured logging with slog. + +#![feature(async_await)] +#![warn( + nonstandard_style, + rust_2018_idioms, + future_incompatible, + missing_debug_implementations +)] + +use slog::{info, o, trace, Drain}; +use slog_async; +use slog_term; + +use futures::future::BoxFuture; +use futures::prelude::*; + +use tide_core::{ + middleware::{Middleware, Next}, + Context, Response, +}; + +/// RequestLogger based on slog.SimpleLogger +#[derive(Debug)] +pub struct RequestLogger { + // drain: dyn slog::Drain, + inner: slog::Logger, +} + +impl RequestLogger { + pub fn new() -> Self { + Default::default() + } + + pub fn with_logger(logger: slog::Logger) -> Self { + Self { inner: logger } + } +} + +impl Default for RequestLogger { + fn default() -> Self { + let decorator = slog_term::TermDecorator::new().build(); + let drain = slog_term::CompactFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + + let log = slog::Logger::root(drain, o!()); + Self { inner: log } + } +} + +/// Stores information during request phase and logs information once the response +/// is generated. +impl Middleware for RequestLogger { + fn handle<'a>(&'a self, cx: Context, next: Next<'a, State>) -> BoxFuture<'a, Response> { + FutureExt::boxed(async move { + let path = cx.uri().path().to_owned(); + let method = cx.method().as_str().to_owned(); + trace!(self.inner, "IN => {} {}", method, path); + let start = std::time::Instant::now(); + let res = next.run(cx).await; + let status = res.status(); + info!( + self.inner, + "{} {} {} {}ms", + method, + path, + status.as_str(), + start.elapsed().as_millis() + ); + res + }) + } +}