actix_web_lab/
lazy_data.rs

1use 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
13/// A lazy extractor for thread-local data.
14///
15/// Using `LazyData` as an extractor will not initialize the data; [`get`](Self::get) must be used.
16pub 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    /// Constructs a new `LazyData` extractor with the given initialization function.
47    ///
48    /// Initialization functions must return a future that resolves to `T`.
49    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    /// Returns reference to result of lazy `T` value, initializing if necessary.
63    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}