Skip to main content

renox_core/
context.rs

1//! Values that belong to the current request or job, such as the current
2//! team of a multi-tenant app, readable anywhere down the call stack
3//! without passing them along (a model's `default_scope`, a listener, a
4//! helper).
5//!
6//! Every request, job, scheduled task and app command runs in a fresh,
7//! empty context. Set a value early (e.g. in an `App::layer` middleware
8//! after the user is known) and read it later:
9//!
10//! ```
11//! use renox::prelude::*;
12//!
13//! #[derive(Clone)]
14//! struct CurrentTeam(i64);
15//!
16//! async fn pick_team(user: Option<AuthUser>, req: Request, next: Next) -> Response {
17//!     if let Some(team) = user.as_ref().and_then(|u| u.get::<i64>("team_id")) {
18//!         renox::context::set(CurrentTeam(team));
19//!     }
20//!     next.run(req).await
21//! }
22//!
23//! # async fn demo() {
24//! // In a job or a command, the value comes from the payload instead:
25//! renox::context::set(CurrentTeam(7));
26//! let team = renox::context::get::<CurrentTeam>().map(|t| t.0); // Some(7)
27//! # assert_eq!(team, Some(7));
28//! # }
29//! # use renox::axum::{extract::Request, middleware::Next};
30//! # let _ = pick_team;
31//! ```
32//!
33//! A context lives in its task: `tokio::spawn` starts without one (wrap the
34//! future in [`scope`] to give it one).
35
36use std::any::{Any, TypeId};
37use std::cell::RefCell;
38use std::collections::HashMap;
39use std::future::Future;
40
41type Values = HashMap<TypeId, Box<dyn Any + Send + Sync>>;
42
43tokio::task_local! {
44    static CONTEXT: RefCell<Values>;
45}
46
47/// Stores `value` in the current context, replacing one of the same type.
48/// Outside a context (a bare `tokio::spawn`), it does nothing and returns
49/// false.
50pub fn set<T: Clone + Send + Sync + 'static>(value: T) -> bool {
51    CONTEXT
52        .try_with(|values| {
53            values
54                .borrow_mut()
55                .insert(TypeId::of::<T>(), Box::new(value));
56        })
57        .is_ok()
58}
59
60/// The value of type `T` in the current context, if one was set.
61pub fn get<T: Clone + Send + Sync + 'static>() -> Option<T> {
62    CONTEXT
63        .try_with(|values| {
64            values
65                .borrow()
66                .get(&TypeId::of::<T>())
67                .and_then(|value| value.downcast_ref::<T>())
68                .cloned()
69        })
70        .ok()
71        .flatten()
72}
73
74/// Removes the value of type `T` from the current context.
75pub fn remove<T: Send + Sync + 'static>() {
76    let _ = CONTEXT.try_with(|values| values.borrow_mut().remove(&TypeId::of::<T>()));
77}
78
79/// Runs `fut` in a fresh, empty context.
80pub async fn scope<F: Future>(fut: F) -> F::Output {
81    CONTEXT.scope(RefCell::new(Values::new()), fut).await
82}
83
84/// A value from the current context as a handler argument, e.g. the team a
85/// middleware picked: `Current(team): Current<CurrentTeam>`. A request
86/// without one is a 500 (a middleware should have set it); take
87/// `Option<Current<T>>` when it's optional.
88///
89/// ```
90/// # use renox::prelude::*;
91/// use renox::context::Current;
92///
93/// #[derive(Clone)]
94/// struct CurrentTeam(i64);
95///
96/// async fn dashboard(Current(team): Current<CurrentTeam>) -> String {
97///     format!("team {}", team.0)
98/// }
99/// ```
100#[derive(Debug, Clone)]
101pub struct Current<T>(pub T);
102
103impl<T, S> axum::extract::FromRequestParts<S> for Current<T>
104where
105    T: Clone + Send + Sync + 'static,
106    S: Send + Sync,
107{
108    type Rejection = crate::Error;
109
110    async fn from_request_parts(
111        _: &mut axum::http::request::Parts,
112        _: &S,
113    ) -> Result<Self, crate::Error> {
114        get::<T>().map(Current).ok_or_else(|| {
115            anyhow::anyhow!(
116                "no `{}` in the request's context: set it in a middleware with renox::context::set",
117                std::any::type_name::<T>()
118            )
119            .into()
120        })
121    }
122}
123
124impl<T, S> axum::extract::OptionalFromRequestParts<S> for Current<T>
125where
126    T: Clone + Send + Sync + 'static,
127    S: Send + Sync,
128{
129    type Rejection = std::convert::Infallible;
130
131    async fn from_request_parts(
132        _: &mut axum::http::request::Parts,
133        _: &S,
134    ) -> Result<Option<Self>, std::convert::Infallible> {
135        Ok(get::<T>().map(Current))
136    }
137}
138
139/// The app of the current request, job, task or command, e.g. for a model
140/// hook that emits an event or forgets a cache key. `None` outside one
141/// (a bare `tokio::spawn`, or a test calling models directly).
142pub fn app() -> Option<crate::AppState> {
143    get::<crate::AppState>()
144}
145
146/// Runs `fut` in a fresh context that knows the app.
147pub(crate) async fn scope_app<F: Future>(state: crate::AppState, fut: F) -> F::Output {
148    scope(async move {
149        set(state);
150        fut.await
151    })
152    .await
153}
154
155/// Runs each request in its own context.
156pub(crate) async fn middleware(
157    axum::extract::State(state): axum::extract::State<crate::AppState>,
158    req: axum::extract::Request,
159    next: axum::middleware::Next,
160) -> axum::response::Response {
161    let info = RequestInfo {
162        method: req.method().to_string(),
163        path: req.uri().path().to_owned(),
164        id: req
165            .extensions()
166            .get::<crate::RequestId>()
167            .map(|id| id.0.clone())
168            .unwrap_or_default(),
169        ip: crate::ClientIp::of(&req).map(|ip| ip.to_string()),
170    };
171    scope_app(state, async move {
172        set(info);
173        next.run(req).await
174    })
175    .await
176}
177
178/// What error reports say about the request they come from.
179#[derive(Debug, Clone)]
180pub(crate) struct RequestInfo {
181    pub method: String,
182    pub path: String,
183    pub id: String,
184    pub ip: Option<String>,
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[derive(Clone, Debug, PartialEq)]
192    struct Team(i64);
193
194    #[tokio::test]
195    async fn values_live_in_their_scope() {
196        assert!(!set(Team(1)), "no context outside a scope");
197        assert_eq!(get::<Team>(), None);
198        scope(async {
199            assert!(set(Team(1)));
200            set(Team(2));
201            assert_eq!(get::<Team>(), Some(Team(2)));
202            assert_eq!(get::<String>(), None);
203            scope(async { assert_eq!(get::<Team>(), None, "a new scope starts empty") }).await;
204            remove::<Team>();
205            assert_eq!(get::<Team>(), None);
206        })
207        .await;
208    }
209}