diff --git a/CHANGES.md b/CHANGES.md index b3d3c180..9ea3a209 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,17 @@ # Changes ## Unreleased - 2020-xx-xx -* Implement Logger middleware regex exclude pattern [#1723] -* Print unconfigured `Data` type when attempting extraction. [#1743] +### Added +* Implement `exclude_regex` for Logger middleware. [#1723] +* Add request-local data extractor `web::ReqData`. [#1748] + +### Changed +* Print non-configured `Data` type when attempting extraction. [#1743] [#1723]: https://github.com/actix/actix-web/pull/1723 [#1743]: https://github.com/actix/actix-web/pull/1743 +[#1748]: https://github.com/actix/actix-web/pull/1748 + ## 3.1.0 - 2020-09-29 ### Changed diff --git a/src/data.rs b/src/data.rs index 01d36569..19c258ff 100644 --- a/src/data.rs +++ b/src/data.rs @@ -20,25 +20,20 @@ pub(crate) type FnDataFactory = /// Application data. /// -/// Application data is an arbitrary data attached to the app. -/// Application data is available to all routes and could be added -/// during application configuration process -/// with `App::data()` method. +/// Application level data is a piece of arbitrary data attached to the app, scope, or resource. +/// Application data is available to all routes and can be added during the application +/// configuration process via `App::data()`. /// -/// Application data could be accessed by using `Data` -/// extractor where `T` is data type. +/// Application data can be accessed by using `Data` extractor where `T` is data type. /// -/// **Note**: http server accepts an application factory rather than -/// an application instance. Http server constructs an application -/// instance for each thread, thus application data must be constructed -/// multiple times. If you want to share data between different -/// threads, a shareable object should be used, e.g. `Send + Sync`. Application -/// data does not need to be `Send` or `Sync`. Internally `Data` type -/// uses `Arc`. if your data implements `Send` + `Sync` traits you can -/// use `web::Data::new()` and avoid double `Arc`. +/// **Note**: http server accepts an application factory rather than an application instance. HTTP +/// server constructs an application instance for each thread, thus application data must be +/// constructed multiple times. If you want to share data between different threads, a shareable +/// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send` +/// or `Sync`. Internally `Data` uses `Arc`. /// -/// If route data is not set for a handler, using `Data` extractor would -/// cause *Internal Server Error* response. +/// If route data is not set for a handler, using `Data` extractor would cause *Internal +/// Server Error* response. /// /// ```rust /// use std::sync::Mutex; @@ -48,7 +43,7 @@ pub(crate) type FnDataFactory = /// counter: usize, /// } /// -/// /// Use `Data` extractor to access data in handler. +/// /// Use the `Data` extractor to access data in a handler. /// async fn index(data: web::Data>) -> impl Responder { /// let mut data = data.lock().unwrap(); /// data.counter += 1; @@ -71,10 +66,6 @@ pub struct Data(Arc); impl Data { /// Create new `Data` instance. - /// - /// Internally `Data` type uses `Arc`. if your data implements - /// `Send` + `Sync` traits you can use `web::Data::new()` and - /// avoid double `Arc`. pub fn new(state: T) -> Data { Data(Arc::new(state)) } diff --git a/src/lib.rs b/src/lib.rs index 327cba95..088444e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,6 +81,7 @@ mod handler; mod info; pub mod middleware; mod request; +mod request_data; mod resource; mod responder; mod rmap; diff --git a/src/request_data.rs b/src/request_data.rs new file mode 100644 index 00000000..c0193041 --- /dev/null +++ b/src/request_data.rs @@ -0,0 +1,175 @@ +use std::{any::type_name, ops::Deref}; + +use actix_http::error::{Error, ErrorInternalServerError}; +use futures_util::future; + +use crate::{dev::Payload, FromRequest, HttpRequest}; + +/// Request-local data extractor. +/// +/// Request-local data is arbitrary data attached to an individual request, usually +/// by middleware. It can be set via `extensions_mut` on [`HttpRequest`][htr_ext_mut] +/// or [`ServiceRequest`][srv_ext_mut]. +/// +/// Unlike app data, request data is dropped when the request has finished processing. This makes it +/// useful as a kind of messaging system between middleware and request handlers. It uses the same +/// types-as-keys storage system as app data. +/// +/// # Mutating Request Data +/// Note that since extractors must output owned data, only types that `impl Clone` can use this +/// extractor. A clone is taken of the required request data and can, therefore, not be directly +/// mutated in-place. To mutate request data, continue to use [`HttpRequest::extensions_mut`] or +/// re-insert the cloned data back into the extensions map. A `DerefMut` impl is intentionally not +/// provided to make this potential foot-gun more obvious. +/// +/// # Example +/// ```rust,no_run +/// # use actix_web::{web, HttpResponse, HttpRequest, Responder}; +/// +/// #[derive(Debug, Clone, PartialEq)] +/// struct FlagFromMiddleware(String); +/// +/// /// Use the `ReqData` extractor to access request data in a handler. +/// async fn handler( +/// req: HttpRequest, +/// opt_flag: Option>, +/// ) -> impl Responder { +/// // use an optional extractor if the middleware is +/// // not guaranteed to add this type of requests data +/// if let Some(flag) = opt_flag { +/// assert_eq!(&flag.into_inner(), req.extensions().get::().unwrap()); +/// } +/// +/// HttpResponse::Ok() +/// } +/// ``` +/// +/// [htr_ext_mut]: crate::HttpRequest::extensions_mut +/// [srv_ext_mut]: crate::dev::ServiceRequest::extensions_mut +#[derive(Debug, Clone)] +pub struct ReqData(T); + +impl ReqData { + /// Consumes the `ReqData`, returning it's wrapped data. + pub fn into_inner(self) -> T { + self.0 + } +} + +impl Deref for ReqData { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl FromRequest for ReqData { + type Config = (); + type Error = Error; + type Future = future::Ready>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + if let Some(st) = req.extensions().get::() { + future::ok(ReqData(st.clone())) + } else { + log::debug!( + "Failed to construct App-level ReqData extractor. \ + Request path: {:?} (type: {})", + req.path(), + type_name::(), + ); + future::err(ErrorInternalServerError( + "Missing expected request extension data", + )) + } + } +} + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, rc::Rc}; + + use futures_util::TryFutureExt as _; + + use super::*; + use crate::{ + dev::Service, + http::{Method, StatusCode}, + test::{init_service, TestRequest}, + web, App, HttpMessage, HttpResponse, + }; + + #[actix_rt::test] + async fn req_data_extractor() { + let mut srv = init_service( + App::new() + .wrap_fn(|req, srv| { + if req.method() == Method::POST { + req.extensions_mut().insert(42u32); + } + + srv.call(req) + }) + .service(web::resource("/test").to( + |req: HttpRequest, data: Option>| { + if req.method() != Method::POST { + assert!(data.is_none()); + } + + if let Some(data) = data { + assert_eq!(*data, 42); + assert_eq!( + Some(data.into_inner()), + req.extensions().get::().copied() + ); + } + + HttpResponse::Ok() + }, + )), + ) + .await; + + let req = TestRequest::get().uri("/test").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::post().uri("/test").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[actix_rt::test] + async fn req_data_internal_mutability() { + let mut srv = init_service( + App::new() + .wrap_fn(|req, srv| { + let data_before = Rc::new(RefCell::new(42u32)); + req.extensions_mut().insert(data_before); + + srv.call(req).map_ok(|res| { + { + let ext = res.request().extensions(); + let data_after = ext.get::>>().unwrap(); + assert_eq!(*data_after.borrow(), 53u32); + } + + res + }) + }) + .default_service(web::to(|data: ReqData>>| { + assert_eq!(*data.borrow(), 42); + *data.borrow_mut() += 11; + assert_eq!(*data.borrow(), 53); + + HttpResponse::Ok() + })), + ) + .await; + + let req = TestRequest::get().uri("/test").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } +} diff --git a/src/web.rs b/src/web.rs index 1d1174f4..ee895f8e 100644 --- a/src/web.rs +++ b/src/web.rs @@ -19,6 +19,7 @@ use crate::service::WebService; pub use crate::config::ServiceConfig; pub use crate::data::Data; pub use crate::request::HttpRequest; +pub use crate::request_data::ReqData; pub use crate::types::*; /// Create resource for a specific path.