actix_web_lab/
lazy_data.rs

1use 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
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        f.debug_struct("Lazy")
36            .field("cell", &self.inner.cell)
37            .field("fut", &"..")
38            .finish()
39    }
40}
41
42impl<T> LazyData<T> {
43    /// Constructs a new `LazyData` extractor with the given initialization function.
44    ///
45    /// Initialization functions must return a future that resolves to `T`.
46    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    /// Returns reference to result of lazy `T` value, initializing if necessary.
60    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}