diff --git a/examples/body_types.rs b/examples/body_types.rs index d6b9e628b..2a5c4a018 100644 --- a/examples/body_types.rs +++ b/examples/body_types.rs @@ -52,7 +52,5 @@ fn main() { app.at("/echo/json").post(echo_json); app.at("/echo/form").post(echo_form); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address); + app.serve(); } diff --git a/examples/catch_all.rs b/examples/catch_all.rs index bfffe1e7a..0dd7bb360 100644 --- a/examples/catch_all.rs +++ b/examples/catch_all.rs @@ -10,7 +10,5 @@ fn main() { router.at("*").get(echo_path); }); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address); + app.serve(); } diff --git a/examples/computed_values.rs b/examples/computed_values.rs index f84cce151..50bf8504b 100644 --- a/examples/computed_values.rs +++ b/examples/computed_values.rs @@ -34,7 +34,5 @@ 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(address); + app.serve(); } diff --git a/examples/configuration.rs b/examples/configuration.rs new file mode 100644 index 000000000..f376ae06e --- /dev/null +++ b/examples/configuration.rs @@ -0,0 +1,34 @@ +#![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/default_handler.rs b/examples/default_handler.rs index 5f78a001b..6472b0a15 100644 --- a/examples/default_handler.rs +++ b/examples/default_handler.rs @@ -9,7 +9,5 @@ fn main() { app.default_handler(async || "¯\\_(ツ)_/¯".with_status(StatusCode::NOT_FOUND)); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address) + app.serve() } diff --git a/examples/default_headers.rs b/examples/default_headers.rs index 40375f91a..01eb5d6dd 100644 --- a/examples/default_headers.rs +++ b/examples/default_headers.rs @@ -13,7 +13,5 @@ fn main() { app.at("/").get(async || "Hello, world!"); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address); + app.serve(); } diff --git a/examples/graphql.rs b/examples/graphql.rs index f1283681e..525a08b58 100644 --- a/examples/graphql.rs +++ b/examples/graphql.rs @@ -70,7 +70,5 @@ fn main() { app.at("/graphql").post(handle_graphql); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address); + app.serve(); } diff --git a/examples/hello.rs b/examples/hello.rs index c3ff2988f..b3e113c8e 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -4,7 +4,5 @@ fn main() { let mut app = tide::App::new(()); app.at("/").get(async || "Hello, world!"); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address); + app.serve(); } diff --git a/examples/messages.rs b/examples/messages.rs index 4ef467eda..e4bc4bb30 100644 --- a/examples/messages.rs +++ b/examples/messages.rs @@ -81,7 +81,5 @@ fn main() { app.at("/message/{}").get(get_message); app.at("/message/{}").post(set_message); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address); + app.serve(); } diff --git a/examples/multipart-form/main.rs b/examples/multipart-form/main.rs index 3c407ff6d..070749582 100644 --- a/examples/multipart-form/main.rs +++ b/examples/multipart-form/main.rs @@ -65,9 +65,7 @@ fn main() { app.at("/upload_file").post(upload_file); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address); + app.serve(); } // Test with: diff --git a/examples/named_path.rs b/examples/named_path.rs index d7b1fc89d..678cb6c10 100644 --- a/examples/named_path.rs +++ b/examples/named_path.rs @@ -25,7 +25,5 @@ fn main() { let mut app = tide::App::new(()); app.at("add_two/{num}").get(add_two); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address); + app.serve(); } diff --git a/examples/simple_nested_router.rs b/examples/simple_nested_router.rs index 5b4b0828e..c7a9787b2 100644 --- a/examples/simple_nested_router.rs +++ b/examples/simple_nested_router.rs @@ -34,7 +34,5 @@ fn main() { let mut app = tide::App::new(()); app.at("add_two").nest(build_add_two); - let address = "127.0.0.1:8000".to_owned(); - println!("Server is listening on http://{}", address); - app.serve(address); + app.serve(); } diff --git a/src/app.rs b/src/app.rs index 54daff034..968ea467a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,17 +5,20 @@ use futures::{ }; use hyper::service::Service; use std::{ + any::Any, + fmt::Debug, ops::{Deref, DerefMut}, sync::Arc, }; use crate::{ body::Body, + configuration::{Configuration, Store}, endpoint::BoxedEndpoint, endpoint::Endpoint, extract::Extract, middleware::{logger::RootLogger, RequestContext}, - router::{Resource, RouteResult, Router}, + router::{EndpointData, Resource, RouteResult, Router}, Middleware, Request, Response, RouteMatch, }; @@ -35,7 +38,7 @@ use crate::{ /// /// let mut app = tide::App::new(()); /// app.at("/hello").get(async || "Hello, world!"); -/// app.serve("127.0.0.1:7878") +/// app.serve() /// ``` /// /// `App` state can be modeled with an underlying `Data` handle for a cloneable type `T`. Endpoints @@ -74,7 +77,7 @@ use crate::{ /// fn main() { /// let mut app = tide::App::new(Database::new()); /// app.at("/messages/insert").post(insert); -/// app.serve("127.0.0.1:7878") +/// app.serve() /// } /// ``` /// @@ -84,7 +87,7 @@ use crate::{ pub struct App { data: Data, router: Router, - default_handler: BoxedEndpoint, + default_handler: EndpointData, } impl App { @@ -94,14 +97,25 @@ impl App { let mut app = App { data, router: Router::new(), - default_handler: BoxedEndpoint::new(async || http::status::StatusCode::NOT_FOUND), + 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 @@ -114,9 +128,16 @@ impl App { } /// 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 Self { - self.default_handler = BoxedEndpoint::new(handler); - self + pub fn default_handler, U>( + &mut self, + handler: T, + ) -> &mut EndpointData { + let endpoint = EndpointData { + endpoint: BoxedEndpoint::new(handler), + store: self.router.store_base.clone(), + }; + self.default_handler = endpoint; + &mut self.default_handler } /// Apply `middleware` to the whole app. Note that the order of nesting subrouters and applying @@ -126,7 +147,18 @@ impl App { self } - fn into_server(self) -> Server { + /// Add a default configuration `item` for the whole app. + pub fn config(&mut self, item: T) -> &mut Self { + self.router.config(item); + self + } + + pub fn get_item(&self) -> Option<&T> { + self.router.get_item() + } + + fn into_server(mut self) -> Server { + self.router.apply_default_config(); Server { data: self.data, router: Arc::new(self.router), @@ -137,12 +169,17 @@ impl App { /// Start serving the app at the given address. /// /// Blocks the calling thread indefinitely. - pub fn serve(self, addr: A) { + pub fn serve(self) { + let configuration = self.get_item::().unwrap(); + let addr = format!("{}:{}", configuration.address, configuration.port) + .parse::() + .unwrap(); + + println!("Server is listening on: http://{}", addr); + let server: Server = self.into_server(); // TODO: be more robust - let addr = addr.to_socket_addrs().unwrap().next().unwrap(); - let server = hyper::Server::bind(&addr) .serve(move || { let res: Result<_, std::io::Error> = Ok(server.clone()); @@ -162,7 +199,7 @@ impl App { struct Server { data: Data, router: Arc>, - default_handler: Arc>, + default_handler: Arc>, } impl Service for Server { @@ -223,7 +260,12 @@ impl DerefMut for AppData { impl Extract for AppData { type Fut = future::Ready>; - fn extract(data: &mut T, req: &mut Request, params: &Option>) -> Self::Fut { + fn extract( + data: &mut T, + req: &mut Request, + params: &Option>, + store: &Store, + ) -> Self::Fut { future::ok(AppData(data.clone())) } } diff --git a/src/body.rs b/src/body.rs index 51c5f5a97..fd4add036 100644 --- a/src/body.rs +++ b/src/body.rs @@ -33,7 +33,7 @@ //! app.at("/echo/string_lossy").post(echo_string_lossy); //! app.at("/echo/bytes").post(echo_bytes); //! -//! # app.serve("127.0.0.1:7878"); +//! # app.serve(); //! # } //! //! ``` @@ -68,7 +68,7 @@ //! app.at("/echo/json").post(echo_json); //! app.at("/echo/form").post(echo_form); //! # -//! # app.serve("127.0.0.1:7878"); +//! # app.serve(); //! # } //! //! ``` @@ -80,7 +80,7 @@ use pin_utils::pin_mut; use std::io::Cursor; use std::ops::{Deref, DerefMut}; -use crate::{Extract, IntoResponse, Request, Response, RouteMatch}; +use crate::{configuration::Store, Extract, IntoResponse, Request, Response, RouteMatch}; /// The raw contents of an http request or response. /// @@ -202,7 +202,12 @@ 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>) -> Self::Fut { + 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="; @@ -248,7 +253,12 @@ impl Extract for // Note: cannot use `existential type` here due to ICE type Fut = FutureObj<'static, Result>; - fn extract(data: &mut S, req: &mut Request, params: &Option>) -> Self::Fut { + fn extract( + data: &mut S, + req: &mut Request, + params: &Option>, + store: &Store, + ) -> Self::Fut { let mut body = std::mem::replace(req.body_mut(), Body::empty()); FutureObj::new(Box::new( async move { @@ -295,7 +305,12 @@ impl Extract for // Note: cannot use `existential type` here due to ICE type Fut = FutureObj<'static, Result>; - fn extract(data: &mut S, req: &mut Request, params: &Option>) -> Self::Fut { + fn extract( + data: &mut S, + req: &mut Request, + params: &Option>, + store: &Store, + ) -> Self::Fut { let mut body = std::mem::replace(req.body_mut(), Body::empty()); FutureObj::new(Box::new( async move { @@ -338,7 +353,12 @@ pub struct Str(pub String); impl Extract for Str { type Fut = FutureObj<'static, Result>; - fn extract(data: &mut S, req: &mut Request, params: &Option>) -> Self::Fut { + fn extract( + data: &mut S, + req: &mut Request, + params: &Option>, + store: &Store, + ) -> Self::Fut { let mut body = std::mem::replace(req.body_mut(), Body::empty()); FutureObj::new(Box::new( @@ -369,7 +389,12 @@ pub struct StrLossy(pub String); impl Extract for StrLossy { type Fut = FutureObj<'static, Result>; - fn extract(data: &mut S, req: &mut Request, params: &Option>) -> Self::Fut { + fn extract( + data: &mut S, + req: &mut Request, + params: &Option>, + store: &Store, + ) -> Self::Fut { let mut body = std::mem::replace(req.body_mut(), Body::empty()); FutureObj::new(Box::new( @@ -400,7 +425,12 @@ pub struct Bytes(pub Vec); impl Extract for Bytes { type Fut = FutureObj<'static, Result>; - fn extract(data: &mut S, req: &mut Request, params: &Option>) -> Self::Fut { + fn extract( + data: &mut S, + req: &mut Request, + params: &Option>, + store: &Store, + ) -> Self::Fut { let mut body = std::mem::replace(req.body_mut(), Body::empty()); FutureObj::new(Box::new( diff --git a/src/configuration/default_config.rs b/src/configuration/default_config.rs new file mode 100644 index 000000000..fd75c6f68 --- /dev/null +++ b/src/configuration/default_config.rs @@ -0,0 +1,79 @@ +/// 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 new file mode 100644 index 000000000..519648460 --- /dev/null +++ b/src/configuration/mod.rs @@ -0,0 +1,156 @@ +//! 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}; + +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/endpoint.rs b/src/endpoint.rs index 98b3a6d20..3b97ba38e 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -1,6 +1,8 @@ use futures::future::{Future, FutureObj}; -use crate::{extract::Extract, head::Head, IntoResponse, Request, Response, RouteMatch}; +use crate::{ + configuration::Store, extract::Extract, head::Head, IntoResponse, Request, Response, RouteMatch, +}; /// The raw representation of an endpoint. /// @@ -25,7 +27,7 @@ use crate::{extract::Extract, head::Head, IntoResponse, Request, Response, Route /// fn main() { /// let mut app = tide::App::new(()); /// app.at("/hello").get(hello); -/// app.serve("127.0.0.1:7878") +/// app.serve() /// } /// ``` /// @@ -62,7 +64,7 @@ use crate::{extract::Extract, head::Head, IntoResponse, Request, Response, Route /// fn main() { /// let mut app = tide::App::new(Database::new()); /// app.at("/messages/insert").post(insert); -/// app.serve("127.0.0.1:7878") +/// app.serve() /// } /// ``` /// @@ -73,11 +75,17 @@ pub trait Endpoint: Send + Sync + 'static { 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>) -> Self::Fut; + fn call( + &self, + data: Data, + req: Request, + params: Option>, + store: &Store, + ) -> Self::Fut; } type BoxedEndpointFn = - dyn Fn(Data, Request, Option) -> FutureObj<'static, Response> + Send + Sync; + dyn Fn(Data, Request, Option, &Store) -> FutureObj<'static, Response> + Send + Sync; pub(crate) struct BoxedEndpoint { endpoint: Box>, @@ -89,8 +97,8 @@ impl BoxedEndpoint { T: Endpoint, { BoxedEndpoint { - endpoint: Box::new(move |data, request, params| { - FutureObj::new(Box::new(ep.call(data, request, params))) + endpoint: Box::new(move |data, request, params, store| { + FutureObj::new(Box::new(ep.call(data, request, params, store))) }), } } @@ -100,8 +108,9 @@ impl BoxedEndpoint { data: Data, req: Request, params: Option>, + store: &Store, ) -> FutureObj<'static, Response> { - (self.endpoint)(data, req, params) + (self.endpoint)(data, req, params, store) } } @@ -133,9 +142,9 @@ macro_rules! end_point_impl_raw { type Fut = FutureObj<'static, Response>; #[allow(unused_mut, non_snake_case)] - fn call(&self, mut data: Data, mut req: Request, params: Option>) -> Self::Fut { + 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);)* + $(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); diff --git a/src/extract.rs b/src/extract.rs index 00218d04b..d0988a451 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1,6 +1,6 @@ use futures::prelude::*; -use crate::{Request, Response, RouteMatch}; +use crate::{configuration::Store, Request, Response, RouteMatch}; /// An extractor for an app with `Data` pub trait Extract: Send + Sized + 'static { @@ -11,5 +11,10 @@ pub trait Extract: Send + Sized + 'static { type Fut: Future> + Send + 'static; /// Attempt to extract a value from the given request. - fn extract(data: &mut Data, req: &mut Request, params: &Option>) -> Self::Fut; + fn extract( + data: &mut Data, + req: &mut Request, + params: &Option>, + store: &Store, + ) -> Self::Fut; } diff --git a/src/head.rs b/src/head.rs index 7d092c18f..b96e9b530 100644 --- a/src/head.rs +++ b/src/head.rs @@ -7,7 +7,7 @@ use futures::future; use std::ops::{Deref, DerefMut}; use std::sync::Arc; -use crate::{Extract, IntoResponse, Request, Response, RouteMatch}; +use crate::{configuration::Store, Extract, IntoResponse, Request, Response, RouteMatch}; /// Header and metadata for a request. /// @@ -74,7 +74,7 @@ impl Head { /// fn main() { /// let mut app = tide::App::new(()); /// app.at("/path/{}").get(path_segment); -/// app.serve("127.0.0.1:7878") +/// app.serve() /// } /// ``` /// @@ -98,7 +98,12 @@ struct PathIdx(usize); impl Extract for Path { type Fut = future::Ready>; - fn extract(data: &mut S, req: &mut Request, params: &Option>) -> Self::Fut { + 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 { @@ -154,7 +159,7 @@ pub trait NamedSegment: Send + 'static + std::str::FromStr { /// fn main() { /// let mut app = tide::App::new(()); /// app.at("/path_named/{num}").get(named_segments); -/// app.serve("127.0.0.1:7878") +/// app.serve() /// } /// ``` /// @@ -176,7 +181,12 @@ impl DerefMut for Named { impl Extract for Named { type Fut = future::Ready>; - fn extract(data: &mut S, req: &mut Request, params: &Option>) -> Self::Fut { + fn extract( + data: &mut S, + req: &mut Request, + params: &Option>, + store: &Store, + ) -> Self::Fut { match params { Some(params) => params .map @@ -201,7 +211,12 @@ where S: 'static, { type Fut = future::Ready>; - fn extract(data: &mut S, req: &mut Request, params: &Option>) -> Self::Fut { + 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 823980f57..454a75c11 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ //! mod app; pub mod body; +pub mod configuration; mod endpoint; mod extract; pub mod head; @@ -23,6 +24,7 @@ mod router; pub use crate::{ app::{App, AppData}, + configuration::ExtractConfiguration, endpoint::Endpoint, extract::Extract, middleware::Middleware, diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index fc839cb43..03434509b 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,8 +1,10 @@ +use std::any::Any; +use std::fmt::Debug; use std::sync::Arc; use futures::future::FutureObj; -use crate::{endpoint::BoxedEndpoint, Request, Response, RouteMatch}; +use crate::{configuration::Store, router::EndpointData, Request, Response, RouteMatch}; mod default_headers; pub mod logger; @@ -28,21 +30,35 @@ pub struct RequestContext<'a, Data> { pub app_data: Data, pub req: Request, pub params: Option>, - pub(crate) endpoint: &'a BoxedEndpoint, + pub(crate) endpoint: &'a EndpointData, pub(crate) next_middleware: &'a [Arc + Send + Sync>], } 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> { if let Some((current, next)) = self.next_middleware.split_first() { self.next_middleware = next; current.handle(self) } else { - FutureObj::new(Box::new(self.endpoint.call( + FutureObj::new(Box::new(self.endpoint.endpoint.call( self.app_data.clone(), self.req, self.params, + &self.endpoint.store, ))) } } diff --git a/src/request.rs b/src/request.rs index c49c951e4..da6e56762 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,7 +1,7 @@ use futures::future; use std::ops::{Deref, DerefMut}; -use crate::{body::Body, Extract, Response, RouteMatch}; +use crate::{body::Body, configuration::Store, Extract, Response, RouteMatch}; /// An HTTP request. /// @@ -53,7 +53,12 @@ impl DerefMut for Computed { impl Extract for Computed { type Fut = future::Ready>; - fn extract(data: &mut Data, req: &mut Request, params: &Option>) -> Self::Fut { + fn extract( + data: &mut Data, + req: &mut Request, + params: &Option>, + store: &Store, + ) -> Self::Fut { future::ok(Computed(T::compute(req))) } } diff --git a/src/router.rs b/src/router.rs index 577a94f5d..5168603df 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,7 +1,10 @@ +use std::any::Any; use std::collections::HashMap; +use std::fmt::Debug; use std::sync::Arc; use crate::{ + configuration::Store, endpoint::{BoxedEndpoint, Endpoint}, Middleware, }; @@ -13,10 +16,11 @@ use path_table::{PathTable, RouteMatch}; pub struct Router { table: PathTable>, middleware_base: Vec + Send + Sync>>, + pub(crate) store_base: Store, } pub(crate) struct RouteResult<'a, Data> { - pub(crate) endpoint: &'a BoxedEndpoint, + pub(crate) endpoint: &'a EndpointData, pub(crate) params: Option>, pub(crate) middleware: &'a [Arc + Send + Sync>], } @@ -44,7 +48,7 @@ fn route_match_success<'a, Data>( } fn route_match_failure<'a, Data>( - endpoint: &'a BoxedEndpoint, + endpoint: &'a EndpointData, middleware: &'a [Arc + Send + Sync>], ) -> RouteResult<'a, Data> { RouteResult { @@ -111,6 +115,7 @@ impl Router { Router { table: PathTable::new(), middleware_base: Vec::new(), + store_base: Store::new(), } } @@ -151,11 +156,19 @@ impl Router { 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>, + default_handler: &'a Arc>, ) -> RouteResult<'a, Data> { match self.table.route(path) { Some((route, route_match)) => route_match_success(route, route_match, method) @@ -165,18 +178,51 @@ impl Router { } } +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>, + endpoints: HashMap>, middleware: Vec + Send + Sync>>, } @@ -192,13 +238,19 @@ impl<'a, Data> Resource<'a, Data> { 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) { + 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 { @@ -209,55 +261,61 @@ impl<'a, Data> Resource<'a, Data> { } let resource = resource.as_mut().unwrap(); - if resource.endpoints.contains_key(&method) { - panic!("A {} endpoint already exists for this path", method) + 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()) } - resource.endpoints.insert(method, BoxedEndpoint::new(ep)); + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + pub fn trace, U>(&mut self, ep: T) -> &mut EndpointData { self.method(http::Method::TRACE, ep) } } @@ -280,9 +338,10 @@ mod tests { path: &'a str, method: &'a http::Method, ) -> Option { - let default_handler = Arc::new(BoxedEndpoint::new(async || { - http::status::StatusCode::NOT_FOUND - })); + let default_handler = Arc::new(EndpointData { + endpoint: BoxedEndpoint::new(async || http::status::StatusCode::NOT_FOUND), + store: Store::new(), + }); let RouteResult { endpoint, params, @@ -311,9 +370,10 @@ mod tests { path: &str, method: &http::Method, ) -> Option { - let default_handler = Arc::new(BoxedEndpoint::new(async || { - http::status::StatusCode::NOT_FOUND - })); + 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()) } @@ -392,9 +452,9 @@ mod tests { #[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").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)] { @@ -413,9 +473,9 @@ mod tests { #[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").nest(|router| { + router.at("/b").get(async || ""); + }); // flattened into /a/b router.at("/a/b").get(async || "duplicate"); } @@ -493,4 +553,84 @@ mod tests { 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().read_to_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().read_to_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().read_to_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().read_to_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().read_to_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().read_to_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().read_to_vec()).unwrap(); + assert_eq!(&*body, &*b"bar"); + } }