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