rainier_container/facade.rs
1//! Facades — static proxies onto container-resolved services.
2//!
3//! Laravel's facades work because PHP can intercept an unknown static call and
4//! forward it to an object pulled from the container. Rust has no such hook, so
5//! a Rainier facade is a zero-sized type with inherent methods that each begin
6//! by resolving the underlying service:
7//!
8//! ```ignore
9//! pub struct Queue;
10//! impl Facade for Queue { type Accessor = QueueManager; }
11//!
12//! impl Queue {
13//! pub async fn push(job: impl Job) -> Result<JobId> {
14//! Self::instance().push(job).await // <- resolve, then delegate
15//! }
16//! }
17//! ```
18//!
19//! The convenience is the same and so is the cost: a facade reaches into
20//! **global** state, which makes the dependency invisible at the call site.
21//! Prefer taking the service as a constructor argument in anything you intend
22//! to unit-test; reach for the facade in application code and route closures,
23//! where the ergonomics win.
24//!
25//! ## Testing
26//!
27//! There is no separate `swap` mechanism, because there does not need to be:
28//! facades resolve through the container on *every* call, so rebinding the
29//! accessor swaps what every facade call sees from that point on.
30//!
31//! ```ignore
32//! app.instance(MailManager::fake()); // every `Mail::…` call now hits the fake
33//! ```
34
35use std::any::type_name;
36use std::sync::{Arc, RwLock};
37
38use std::cell::RefCell;
39use std::marker::PhantomData;
40
41use crate::application::Application;
42
43/// The application backing every facade in this process.
44static FACADE_APP: RwLock<Option<Arc<Application>>> = RwLock::new(None);
45
46thread_local! {
47 /// Applications scoped to *this thread*, innermost last.
48 ///
49 /// A stack rather than an `Option` so scopes nest, and so dropping an
50 /// inner one restores the outer rather than clearing everything.
51 static SCOPED_APPS: RefCell<Vec<Arc<Application>>> = const { RefCell::new(Vec::new()) };
52}
53
54tokio::task_local! {
55 /// The application scoped to *this task*.
56 ///
57 /// A task-local rather than a second thread-local, because a task is the
58 /// thing that outlives a thread: tokio may resume a future on a different
59 /// worker between polls, and a thread-local does not follow it.
60 static TASK_APP: Arc<Application>;
61}
62
63/// A facade scope, undone when it is dropped.
64///
65/// Held by whoever called [`scope_facade_application`]. Dropping it is the only
66/// way to leave the scope, so a panicking test cannot leak its application into
67/// the next one.
68#[must_use = "the scope ends the moment this is dropped"]
69pub struct FacadeScope {
70 /// Not `Send`: the scope is a thread-local, and moving the guard to another
71 /// thread would pop a stack it was never pushed onto.
72 _not_send: PhantomData<*const ()>,
73}
74
75impl Drop for FacadeScope {
76 fn drop(&mut self) {
77 SCOPED_APPS.with(|apps| {
78 apps.borrow_mut().pop();
79 });
80 }
81}
82
83/// Resolve facades through `app` **on this thread**, until the guard drops.
84///
85/// The answer to a process-global container: two tests can each boot their own
86/// application and each see their own, instead of racing to install one
87/// globally and then resolving out of whichever won.
88///
89/// ```
90/// # use std::sync::Arc;
91/// # use rainier_container::{scope_facade_application, try_facade_application, Application};
92/// let app = Arc::new(Application::new("."));
93/// {
94/// let _scope = scope_facade_application(Arc::clone(&app));
95/// assert!(try_facade_application().is_some());
96/// }
97/// # let _ = app;
98/// ```
99///
100/// # Where a thread scope reaches, and where it stops
101///
102/// It covers everything that runs **on this thread**, which is more than it
103/// sounds: `block_on` drives a future on the calling thread, so the body of a
104/// `#[tokio::test]` stays inside the scope even on a `multi_thread` runtime,
105/// across as many `.await`s as it likes.
106///
107/// It stops at [`tokio::spawn`] and `spawn_blocking`. A spawned task is a new
108/// task on some other thread, and it resolves through the process-wide
109/// application instead — silently. That is what
110/// [`spawn_with_facades`] and [`with_facade_application`] are for.
111///
112/// This does not replace [`set_facade_application`]; it layers over it.
113/// Application code installs one globally at boot; a test scopes its own on
114/// top.
115pub fn scope_facade_application(app: Arc<Application>) -> FacadeScope {
116 SCOPED_APPS.with(|apps| apps.borrow_mut().push(app));
117 FacadeScope { _not_send: PhantomData }
118}
119
120/// The innermost application scoped to this thread, if any.
121pub fn scoped_facade_application() -> Option<Arc<Application>> {
122 SCOPED_APPS.with(|apps| apps.borrow().last().cloned())
123}
124
125/// Resolve facades through `app` for the duration of `future` — **wherever it
126/// runs**.
127///
128/// A task-local scope rather than a thread-local one, so it follows the future
129/// when tokio resumes it on a different worker thread. That is the difference
130/// that matters for anything spawned:
131///
132/// ```
133/// # use std::sync::Arc;
134/// # use rainier_container::{with_facade_application, facade_application, Application};
135/// # #[tokio::main(flavor = "current_thread")] async fn main() {
136/// let app = Arc::new(Application::new("."));
137/// let same = with_facade_application(Arc::clone(&app), async {
138/// facade_application()
139/// })
140/// .await;
141///
142/// assert!(Arc::ptr_eq(&same, &app));
143/// # }
144/// ```
145///
146/// It nests: an inner scope wins for its own duration and the outer one
147/// resumes after.
148///
149/// A task-local is **not inherited** by a task this future goes on to spawn —
150/// nothing in tokio propagates them — so a spawn inside still needs
151/// [`spawn_with_facades`].
152pub async fn with_facade_application<F>(app: Arc<Application>, future: F) -> F::Output
153where
154 F: std::future::Future,
155{
156 TASK_APP.scope(app, future).await
157}
158
159/// The application scoped to this task, if any.
160pub fn task_facade_application() -> Option<Arc<Application>> {
161 TASK_APP.try_with(Arc::clone).ok()
162}
163
164/// [`tokio::spawn`], carrying the current facade application into the new task.
165///
166/// The gap this closes: a spawned task starts with no thread scope and no task
167/// scope, so it resolves through the process-wide application — which in a test
168/// is whichever one booted last, and in a multi-tenant process is the wrong
169/// one. Nothing warns, because from inside the task there is nothing to notice.
170///
171/// ```
172/// # use std::sync::Arc;
173/// # use rainier_container::{scope_facade_application, spawn_with_facades, facade_application, Application};
174/// # #[tokio::main(flavor = "multi_thread")] async fn main() {
175/// let app = Arc::new(Application::new("."));
176/// let _scope = scope_facade_application(Arc::clone(&app));
177///
178/// let seen = spawn_with_facades(async { facade_application() }).await.unwrap();
179///
180/// assert!(Arc::ptr_eq(&seen, &app), "the spawned task saw the same application");
181/// # }
182/// ```
183///
184/// With no application installed at all this is exactly [`tokio::spawn`].
185///
186/// # Panics
187///
188/// Outside a tokio runtime, like [`tokio::spawn`].
189pub fn spawn_with_facades<F>(future: F) -> tokio::task::JoinHandle<F::Output>
190where
191 F: std::future::Future + Send + 'static,
192 F::Output: Send + 'static,
193{
194 match try_facade_application() {
195 Some(app) => tokio::spawn(with_facade_application(app, future)),
196 None => tokio::spawn(future),
197 }
198}
199
200/// Install the application that facades resolve through.
201///
202/// Called once during bootstrap. Calling it again replaces the binding, which
203/// is what lets a test harness stand up a fresh application per test.
204pub fn set_facade_application(app: Arc<Application>) {
205 *FACADE_APP.write().expect("facade application lock poisoned") = Some(app);
206}
207
208/// The application facades resolve through, if one is installed.
209///
210/// Three places are consulted, nearest first:
211///
212/// 1. the [task](with_facade_application) scope — follows a future across
213/// threads;
214/// 2. the [thread](scope_facade_application) scope — covers everything running
215/// on this thread;
216/// 3. the [process](set_facade_application) binding — what the bootstrap
217/// installed.
218///
219/// Nearest-first is what lets a test have its own without the application code
220/// under test knowing anything about it.
221pub fn try_facade_application() -> Option<Arc<Application>> {
222 if let Some(scoped) = task_facade_application() {
223 return Some(scoped);
224 }
225 if let Some(scoped) = scoped_facade_application() {
226 return Some(scoped);
227 }
228 FACADE_APP.read().expect("facade application lock poisoned").clone()
229}
230// The application facades resolve through.
231///
232/// # Panics
233///
234/// If no application has been installed. That is a bootstrap bug — a facade
235/// was used before (or without) [`set_facade_application`] — so it fails loudly
236/// rather than returning an error every call site would have to unwrap.
237pub fn facade_application() -> Arc<Application> {
238 try_facade_application().expect(
239 "no application is bound to the facades — call `set_facade_application` during bootstrap \
240 (or use the service directly instead of the facade)",
241 )
242}
243
244/// Forget the installed application. Mainly for tests that assert on the
245/// unbound behaviour.
246pub fn clear_facade_application() {
247 *FACADE_APP.write().expect("facade application lock poisoned") = None;
248}
249
250/// A static proxy onto a container-resolved service.
251///
252/// Implementors are zero-sized marker types; all the behaviour lives in the
253/// [`Accessor`](Facade::Accessor) they resolve.
254pub trait Facade {
255 /// The service this facade forwards to.
256 type Accessor: Send + Sync + 'static;
257
258 /// Resolve the underlying service.
259 ///
260 /// # Panics
261 ///
262 /// If no application is installed, or the accessor is not bound in it.
263 /// Both are configuration bugs: a facade whose service was never registered
264 /// can never work, so it says so rather than propagating an error into
265 /// every caller.
266 fn instance() -> Arc<Self::Accessor> {
267 let app = facade_application();
268 match app.resolve::<Self::Accessor>() {
269 Ok(service) => service,
270 Err(e) => panic!(
271 "the `{}` facade could not resolve `{}`: {e}",
272 type_name::<Self>(),
273 type_name::<Self::Accessor>()
274 ),
275 }
276 }
277
278 /// Resolve the underlying service, or `None` if the application or the
279 /// binding is missing. For code that must degrade rather than panic.
280 fn try_instance() -> Option<Arc<Self::Accessor>> {
281 try_facade_application()?.try_resolve::<Self::Accessor>()
282 }
283}
284
285/// Declare a facade type and its [`Facade`] impl.
286///
287/// ```
288/// # use rainier_container::{facade, Application, Facade};
289/// # use std::sync::Arc;
290/// pub struct Clock {
291/// pub now: u64,
292/// }
293///
294/// facade!(
295/// /// Reads the application clock.
296/// Time => Clock
297/// );
298///
299/// # fn main() {
300/// let app = Application::new(".");
301/// app.instance(Clock { now: 1 });
302/// rainier_container::set_facade_application(Arc::new(app));
303///
304/// assert_eq!(Time::instance().now, 1);
305/// # rainier_container::clear_facade_application();
306/// # }
307/// ```
308#[macro_export]
309macro_rules! facade {
310 ($(#[$meta:meta])* $name:ident => $accessor:ty) => {
311 $(#[$meta])*
312 #[doc = ""]
313 #[doc = concat!("A [`Facade`](", stringify!($crate), "::Facade) over [`", stringify!($accessor), "`].")]
314 #[derive(Debug, Clone, Copy, Default)]
315 pub struct $name;
316
317 impl $crate::Facade for $name {
318 type Accessor = $accessor;
319 }
320 };
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::Container;
327
328 struct Clock(u64);
329 struct Missing;
330
331 struct Time;
332 impl Facade for Time {
333 type Accessor = Clock;
334 }
335
336 struct Nowhere;
337 impl Facade for Nowhere {
338 type Accessor = Missing;
339 }
340
341 // The facade application is process-global, so these tests must not run
342 // concurrently with one another.
343 static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
344
345 fn with_app(build: impl FnOnce(&Application)) -> std::sync::MutexGuard<'static, ()> {
346 let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
347 let app = Application::new(".");
348 build(&app);
349 set_facade_application(Arc::new(app));
350 guard
351 }
352
353 #[test]
354 fn a_facade_resolves_through_the_container() {
355 let _guard = with_app(|app| app.instance(Clock(5)));
356 assert_eq!(Time::instance().0, 5);
357 clear_facade_application();
358 }
359
360 #[test]
361 fn rebinding_the_accessor_swaps_what_the_facade_sees() {
362 let _guard = with_app(|app| app.instance(Clock(1)));
363 assert_eq!(Time::instance().0, 1);
364
365 // No `swap` API needed: the facade re-resolves on every call.
366 facade_application().instance(Clock(2));
367 assert_eq!(Time::instance().0, 2);
368 clear_facade_application();
369 }
370
371 #[test]
372 fn try_instance_is_none_for_an_unbound_accessor() {
373 let _guard = with_app(|_| {});
374 assert!(Nowhere::try_instance().is_none());
375 clear_facade_application();
376 }
377
378 #[test]
379 fn try_instance_is_none_with_no_application() {
380 let _guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
381 clear_facade_application();
382 assert!(Time::try_instance().is_none());
383 }
384
385 #[test]
386 #[should_panic(expected = "could not resolve")]
387 fn instance_panics_for_an_unbound_accessor() {
388 let _guard = with_app(|_| {});
389 let _ = Nowhere::instance();
390 }
391
392 #[test]
393 fn a_transient_binding_is_re_resolved_each_call() {
394 let _guard = with_app(|app| {
395 app.bind(|_: &Container| Ok(Clock(7)));
396 });
397 let a = Time::instance();
398 let b = Time::instance();
399 assert_eq!(a.0, b.0);
400 assert!(!Arc::ptr_eq(&a, &b));
401 clear_facade_application();
402 }
403
404 #[test]
405 fn a_scope_wins_over_the_process_wide_application() {
406 // The whole point: a test can have its own container without racing
407 // whatever else installed one globally.
408 let global = Arc::new(Application::new("global"));
409 let scoped = Arc::new(Application::new("scoped"));
410
411 set_facade_application(Arc::clone(&global));
412 assert_eq!(facade_application().base_path(), global.base_path());
413
414 {
415 let _scope = scope_facade_application(Arc::clone(&scoped));
416 assert_eq!(facade_application().base_path(), scoped.base_path());
417 }
418
419 assert_eq!(facade_application().base_path(), global.base_path(), "the scope is undone");
420 clear_facade_application();
421 }
422
423 #[test]
424 fn scopes_nest_and_unwind_in_order() {
425 let outer = Arc::new(Application::new("outer"));
426 let inner = Arc::new(Application::new("inner"));
427
428 let _outer = scope_facade_application(Arc::clone(&outer));
429 {
430 let _inner = scope_facade_application(Arc::clone(&inner));
431 assert_eq!(facade_application().base_path(), inner.base_path());
432 }
433 assert_eq!(facade_application().base_path(), outer.base_path());
434 }
435
436 #[test]
437 fn a_scope_that_panicked_does_not_leak_into_the_next_test() {
438 // `Drop` runs while unwinding, which is why the scope is a guard and
439 // not a pair of calls.
440 let scoped = Arc::new(Application::new("panicky"));
441
442 let outcome = std::panic::catch_unwind(|| {
443 let _scope = scope_facade_application(Arc::clone(&scoped));
444 panic!("as a test would");
445 });
446
447 assert!(outcome.is_err());
448 assert!(scoped_facade_application().is_none(), "the stack should be empty again");
449 }
450}