Skip to main content

nest_rs_database/
executor.rs

1use std::any::Any;
2use std::future::Future;
3use std::sync::Arc;
4
5/// An ambient handle to a unit of database work, installed in the
6/// task-local for the lifetime of a request or a worker job.
7///
8/// The trait is **object-safe** so the engine can carry it as
9/// `Arc<dyn Executor>` without naming a concrete ORM. The concrete handle
10/// (a SeaORM `Executor` enum, a `sqlx::Pool`, a `diesel_async::Connection`,
11/// …) implements this trait; an ORM-specific `Repo` recovers the concrete
12/// type via [`Executor::as_any`] when it needs to issue a query.
13///
14/// Downcasting is the documented seam: this crate stays free of every
15/// candidate ORM's query API, and each `Repo` knows exactly which executor
16/// shape its `Module` installs. A downcast miss is a framework bug
17/// (mismatched `Module` + `Repo`) and should `panic!` with a clear message
18/// during boot tests, never surface as a runtime "no rows".
19pub trait Executor: Any + Send + Sync + 'static {
20    /// Downcast handle. Used by an ORM-specific `Repo` to recover its
21    /// concrete executor type from the ambient `Arc<dyn Executor>`.
22    fn as_any(&self) -> &dyn Any;
23}
24
25/// Whether the ambient executor belongs to a request or a worker job. An
26/// ORM's `Repo` reads this back to fail closed when a request path lacks
27/// an ambient authorization context (a missing principal on a worker is
28/// expected — it's system work; on a request it's a bug).
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum ExecutorScope {
31    Request,
32    Job,
33}
34
35tokio::task_local! {
36    static EXECUTOR: Arc<dyn Executor>;
37    static EXECUTOR_SCOPE: ExecutorScope;
38}
39
40/// The installed ambient executor, or `None` outside any scope. An
41/// ORM-specific `Repo` calls this and downcasts via [`Executor::as_any`].
42pub fn current_executor() -> Option<Arc<dyn Executor>> {
43    EXECUTOR.try_with(Arc::clone).ok()
44}
45
46/// The installed ambient executor scope, or `None` outside any scope.
47pub fn current_executor_scope() -> Option<ExecutorScope> {
48    EXECUTOR_SCOPE.try_with(Clone::clone).ok()
49}
50
51/// Install `executor` without tagging a scope. Prefer the request/job
52/// variants at framework boundaries so authorization can distinguish the
53/// two paths.
54pub async fn with_executor<F: Future>(executor: Arc<dyn Executor>, fut: F) -> F::Output {
55    EXECUTOR.scope(executor, fut).await
56}
57
58/// Install `executor` and tag the scope as a request — the path on which a
59/// `Repo` fails closed when no ambient authorization context is present.
60pub async fn with_request_executor<F: Future>(executor: Arc<dyn Executor>, fut: F) -> F::Output {
61    EXECUTOR
62        .scope(executor, EXECUTOR_SCOPE.scope(ExecutorScope::Request, fut))
63        .await
64}
65
66/// Install `executor` and tag the scope as a worker job — the path on
67/// which a `Repo` runs unscoped (no principal ⇒ system work).
68pub async fn with_job_executor<F: Future>(executor: Arc<dyn Executor>, fut: F) -> F::Output {
69    EXECUTOR
70        .scope(executor, EXECUTOR_SCOPE.scope(ExecutorScope::Job, fut))
71        .await
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    struct StubExecutor;
79    impl Executor for StubExecutor {
80        fn as_any(&self) -> &dyn Any {
81            self
82        }
83    }
84
85    fn stub() -> Arc<dyn Executor> {
86        Arc::new(StubExecutor)
87    }
88
89    #[tokio::test]
90    async fn no_ambient_state_outside_any_scope() {
91        assert!(current_executor().is_none());
92        assert!(current_executor_scope().is_none());
93    }
94
95    #[tokio::test]
96    async fn with_executor_installs_but_does_not_tag() {
97        with_executor(stub(), async {
98            assert!(current_executor().is_some());
99            assert!(current_executor_scope().is_none());
100        })
101        .await;
102    }
103
104    #[tokio::test]
105    async fn with_request_executor_tags_request() {
106        with_request_executor(stub(), async {
107            assert_eq!(current_executor_scope(), Some(ExecutorScope::Request));
108            assert!(current_executor().is_some());
109        })
110        .await;
111    }
112
113    #[tokio::test]
114    async fn with_job_executor_tags_job() {
115        with_job_executor(stub(), async {
116            assert_eq!(current_executor_scope(), Some(ExecutorScope::Job));
117            assert!(current_executor().is_some());
118        })
119        .await;
120    }
121
122    #[tokio::test]
123    async fn scope_unwinds_on_exit() {
124        with_request_executor(stub(), async {}).await;
125        assert!(current_executor().is_none());
126        assert!(current_executor_scope().is_none());
127    }
128
129    #[tokio::test]
130    async fn nested_scope_shadows_outer() {
131        with_request_executor(stub(), async {
132            assert_eq!(current_executor_scope(), Some(ExecutorScope::Request));
133            with_job_executor(stub(), async {
134                assert_eq!(current_executor_scope(), Some(ExecutorScope::Job));
135            })
136            .await;
137            assert_eq!(current_executor_scope(), Some(ExecutorScope::Request));
138        })
139        .await;
140    }
141
142    #[tokio::test]
143    async fn downcast_round_trips_the_concrete_type() {
144        with_request_executor(stub(), async {
145            let e = current_executor().expect("installed");
146            assert!(e.as_any().is::<StubExecutor>());
147        })
148        .await;
149    }
150}