actix_web_lab/
lazy_data.rs1use std::{
2 cell::Cell,
3 fmt,
4 future::{ready, Future, Ready},
5 rc::Rc,
6};
7
8use actix_web::{dev, error, Error, FromRequest, HttpRequest};
9use futures_core::future::LocalBoxFuture;
10use tokio::sync::OnceCell;
11use tracing::debug;
12
13pub struct LazyData<T> {
17 inner: Rc<LazyDataInner<T>>,
18}
19
20struct LazyDataInner<T> {
21 cell: OnceCell<T>,
22 fut: Cell<Option<LocalBoxFuture<'static, T>>>,
23}
24
25impl<T> Clone for LazyData<T> {
26 fn clone(&self) -> Self {
27 Self {
28 inner: self.inner.clone(),
29 }
30 }
31}
32
33impl<T: fmt::Debug> fmt::Debug for LazyData<T> {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 f.debug_struct("Lazy")
36 .field("cell", &self.inner.cell)
37 .field("fut", &"..")
38 .finish()
39 }
40}
41
42impl<T> LazyData<T> {
43 pub fn new<F, Fut>(init: F) -> LazyData<T>
47 where
48 F: FnOnce() -> Fut,
49 Fut: Future<Output = T> + 'static,
50 {
51 Self {
52 inner: Rc::new(LazyDataInner {
53 cell: OnceCell::new(),
54 fut: Cell::new(Some(Box::pin(init()))),
55 }),
56 }
57 }
58
59 pub async fn get(&self) -> &T {
61 self.inner
62 .cell
63 .get_or_init(|| async move {
64 match self.inner.fut.take() {
65 Some(fut) => fut.await,
66 None => panic!("LazyData instance has previously been poisoned"),
67 }
68 })
69 .await
70 }
71}
72
73impl<T: 'static> FromRequest for LazyData<T> {
74 type Error = Error;
75 type Future = Ready<Result<Self, Error>>;
76
77 #[inline]
78 fn from_request(req: &HttpRequest, _: &mut dev::Payload) -> Self::Future {
79 if let Some(lazy) = req.app_data::<LazyData<T>>() {
80 ready(Ok(lazy.clone()))
81 } else {
82 debug!(
83 "Failed to extract `LazyData<{}>` for `{}` handler. For the Data extractor to work \
84 correctly, wrap the data with `LazyData::new()` and pass it to `App::app_data()`. \
85 Ensure that types align in both the set and retrieve calls.",
86 core::any::type_name::<T>(),
87 req.match_name().unwrap_or_else(|| req.path())
88 );
89
90 ready(Err(error::ErrorInternalServerError(
91 "Requested application data is not configured correctly. \
92 View/enable debug logs for more details.",
93 )))
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use std::time::Duration;
101
102 use actix_web::{
103 http::StatusCode,
104 test::{call_service, init_service, TestRequest},
105 web, App, HttpResponse,
106 };
107
108 use super::*;
109
110 #[actix_web::test]
111 async fn lazy_data() {
112 let app = init_service(
113 App::new()
114 .app_data(LazyData::new(|| async { 10usize }))
115 .service(web::resource("/").to(|_: LazyData<usize>| HttpResponse::Ok())),
116 )
117 .await;
118 let req = TestRequest::default().to_request();
119 let resp = call_service(&app, req).await;
120 assert_eq!(resp.status(), StatusCode::OK);
121
122 let app = init_service(
123 App::new()
124 .app_data(LazyData::new(|| async {
125 actix_web::rt::time::sleep(Duration::from_millis(40)).await;
126 10usize
127 }))
128 .service(web::resource("/").to(|_: LazyData<usize>| HttpResponse::Ok())),
129 )
130 .await;
131 let req = TestRequest::default().to_request();
132 let resp = call_service(&app, req).await;
133 assert_eq!(resp.status(), StatusCode::OK);
134
135 let app = init_service(
136 App::new()
137 .app_data(LazyData::new(|| async { 10u32 }))
138 .service(web::resource("/").to(|_: LazyData<usize>| HttpResponse::Ok())),
139 )
140 .await;
141 let req = TestRequest::default().to_request();
142 let resp = call_service(&app, req).await;
143 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
144 }
145
146 #[actix_web::test]
147 async fn lazy_data_web_block() {
148 let app = init_service(
149 App::new()
150 .app_data(LazyData::new(|| async {
151 web::block(|| std::thread::sleep(Duration::from_millis(40)))
152 .await
153 .unwrap();
154
155 10usize
156 }))
157 .service(web::resource("/").to(|_: LazyData<usize>| HttpResponse::Ok())),
158 )
159 .await;
160 let req = TestRequest::default().to_request();
161 let resp = call_service(&app, req).await;
162 assert_eq!(resp.status(), StatusCode::OK);
163 }
164}