Skip to content

Commit

Permalink
feat: add ThinData wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
robjtede committed Aug 9, 2024
1 parent 5be5382 commit 4938901
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 1 deletion.
4 changes: 4 additions & 0 deletions actix-web/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

- Add `web::ThinData` extractor.

## 4.8.0

### Added
Expand Down
1 change: 1 addition & 0 deletions actix-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ encoding_rs = "0.8"
futures-core = { version = "0.3.17", default-features = false }
futures-util = { version = "0.3.17", default-features = false }
itoa = "1"
impl-more = "0.1.4"
language-tags = "0.3"
log = "0.4"
mime = "0.3"
Expand Down
1 change: 1 addition & 0 deletions actix-web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ mod scope;
mod server;
mod service;
pub mod test;
mod thin_data;
pub(crate) mod types;
pub mod web;

Expand Down
121 changes: 121 additions & 0 deletions actix-web/src/thin_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::any::type_name;

use actix_utils::future::{ready, Ready};

use crate::{dev::Payload, error, FromRequest, HttpRequest};

/// Application data wrapper and extractor for cheaply-cloned types.
///
/// Similar to the [`Data`] wrapper but for `Clone`/`Copy` types that are already an `Arc` internally,
/// share state using some other means when cloned, or is otherwise static data that is very cheap
/// to clone.
///
/// Unlike `Data`, this wrapper clones `T` during extraction. Therefore, it is the user's
/// responsibility to ensure that clones of `T` do actually share the same state, otherwise state
/// may be unexpectedly different across multiple requests.
///
/// Note that if your type is literally an `Arc<T>` then it's recommended to use the
/// [`Data::from(arc)`][data_from_arc] conversion instead.
///
/// # Examples
///
/// ```
/// use actix_web::{
/// web::{self, ThinData},
/// App, HttpResponse, Responder,
/// };
///
/// // Use the `ThinData<T>` extractor to access a database connection pool.
/// async fn index(ThinData(db_pool): ThinData<DbPool>) -> impl Responder {
/// // database action ...
///
/// HttpResponse::Ok()
/// }
///
/// # type DbPool = ();
/// let db_pool = DbPool::default();
///
/// App::new()
/// .app_data(ThinData(db_pool.clone()))
/// .service(web::resource("/").get(index))
/// # ;
/// ```
///
/// [`Data`]: crate::web::Data
/// [data_from_arc]: crate::web::Data#impl-From<Arc<T>>-for-Data<T>
#[derive(Debug, Clone)]
pub struct ThinData<T>(pub T);

impl_more::impl_as_ref!(ThinData<T> => T);
impl_more::impl_as_mut!(ThinData<T> => T);
impl_more::impl_deref_and_mut!(<T> in ThinData<T> => T);

impl<T: Clone + 'static> FromRequest for ThinData<T> {
type Error = crate::Error;
type Future = Ready<Result<Self, Self::Error>>;

#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(req.app_data::<Self>().cloned().ok_or_else(|| {
log::debug!(
"Failed to extract `ThinData<{}>` for `{}` handler. For the ThinData extractor to work \
correctly, wrap the data with `ThinData()` and pass it to `App::app_data()`. \
Ensure that types align in both the set and retrieve calls.",
type_name::<T>(),
req.match_name().unwrap_or(req.path())
);

error::ErrorInternalServerError(
"Requested application data is not configured correctly. \
View/enable debug logs for more details.",
)
}))
}
}

#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};

use super::*;
use crate::{
http::StatusCode,
test::{call_service, init_service, TestRequest},
web, App, HttpResponse,
};

type TestT = Arc<Mutex<u32>>;

#[actix_rt::test]
async fn thin_data() {
let test_data = TestT::default();

let app = init_service(App::new().app_data(ThinData(test_data.clone())).service(
web::resource("/").to(|td: ThinData<TestT>| {
*td.lock().unwrap() += 1;
HttpResponse::Ok()
}),
))
.await;

for _ in 0..3 {
let req = TestRequest::default().to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}

assert_eq!(*test_data.lock().unwrap(), 3);
}

#[actix_rt::test]
async fn thin_data_missing() {
let app = init_service(
App::new().service(web::resource("/").to(|_: ThinData<u32>| HttpResponse::Ok())),
)
.await;

let req = TestRequest::default().to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
}
4 changes: 3 additions & 1 deletion actix-web/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! # Request Extractors
//! - [`Data`]: Application data item
//! - [`ThinData`]: Cheap-to-clone application data item
//! - [`ReqData`]: Request-local data item
//! - [`Path`]: URL path parameters / dynamic segments
//! - [`Query`]: URL query parameters
Expand All @@ -22,7 +23,8 @@ use actix_router::IntoPatterns;
pub use bytes::{Buf, BufMut, Bytes, BytesMut};

pub use crate::{
config::ServiceConfig, data::Data, redirect::Redirect, request_data::ReqData, types::*,
config::ServiceConfig, data::Data, redirect::Redirect, request_data::ReqData,
thin_data::ThinData, types::*,
};
use crate::{
error::BlockingError, http::Method, service::WebService, FromRequest, Handler, Resource,
Expand Down

0 comments on commit 4938901

Please sign in to comment.