Skip to main content

rainier_container/
facade.rs

1//! Facades — static proxies onto container-resolved services.
2//!
3//! In PHP, a facade 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        //
409        // This one has to hold `SERIAL` to make its own claim, which is the
410        // irony of the thing it is testing: it asserts about the *process*
411        // slot, and there is one of those. Without the guard another test's
412        // `with_app` lands between the two assertions and this fails, seeing
413        // that test's application instead of `global`.
414        let _guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
415
416        let global = Arc::new(Application::new("global"));
417        let scoped = Arc::new(Application::new("scoped"));
418
419        set_facade_application(Arc::clone(&global));
420        assert_eq!(facade_application().base_path(), global.base_path());
421
422        {
423            let _scope = scope_facade_application(Arc::clone(&scoped));
424            assert_eq!(facade_application().base_path(), scoped.base_path());
425        }
426
427        assert_eq!(facade_application().base_path(), global.base_path(), "the scope is undone");
428        clear_facade_application();
429    }
430
431    #[test]
432    fn scopes_nest_and_unwind_in_order() {
433        let outer = Arc::new(Application::new("outer"));
434        let inner = Arc::new(Application::new("inner"));
435
436        let _outer = scope_facade_application(Arc::clone(&outer));
437        {
438            let _inner = scope_facade_application(Arc::clone(&inner));
439            assert_eq!(facade_application().base_path(), inner.base_path());
440        }
441        assert_eq!(facade_application().base_path(), outer.base_path());
442    }
443
444    #[test]
445    fn a_scope_that_panicked_does_not_leak_into_the_next_test() {
446        // `Drop` runs while unwinding, which is why the scope is a guard and
447        // not a pair of calls.
448        let scoped = Arc::new(Application::new("panicky"));
449
450        let outcome = std::panic::catch_unwind(|| {
451            let _scope = scope_facade_application(Arc::clone(&scoped));
452            panic!("as a test would");
453        });
454
455        assert!(outcome.is_err());
456        assert!(scoped_facade_application().is_none(), "the stack should be empty again");
457    }
458}