Skip to main content

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
54/// A facade scope, undone when it is dropped.
55///
56/// Held by whoever called [`scope_facade_application`]. Dropping it is the only
57/// way to leave the scope, so a panicking test cannot leak its application into
58/// the next one.
59#[must_use = "the scope ends the moment this is dropped"]
60pub struct FacadeScope {
61    /// Not `Send`: the scope is a thread-local, and moving the guard to another
62    /// thread would pop a stack it was never pushed onto.
63    _not_send: PhantomData<*const ()>,
64}
65
66impl Drop for FacadeScope {
67    fn drop(&mut self) {
68        SCOPED_APPS.with(|apps| {
69            apps.borrow_mut().pop();
70        });
71    }
72}
73
74/// Resolve facades through `app` **on this thread**, until the guard drops.
75///
76/// The answer to a process-global container: two tests can each boot their own
77/// application and each see their own, instead of racing to install one
78/// globally and then resolving out of whichever won.
79///
80/// ```
81/// # use std::sync::Arc;
82/// # use rainier_container::{scope_facade_application, try_facade_application, Application};
83/// let app = Arc::new(Application::new("."));
84/// {
85///     let _scope = scope_facade_application(Arc::clone(&app));
86///     assert!(try_facade_application().is_some());
87/// }
88/// # let _ = app;
89/// ```
90///
91/// # It is per-thread, and that is a real limit
92///
93/// `#[tokio::test]` runs on a current-thread runtime, so a test and everything
94/// it awaits stay on one thread and see the scope. A **multi-threaded** runtime
95/// may move a future between threads, and after the move a facade resolves
96/// through the process-wide application instead.
97///
98/// Which is why this does not replace [`set_facade_application`] — it layers
99/// over it. Application code installs one globally at boot; a test scopes its
100/// own on top.
101pub fn scope_facade_application(app: Arc<Application>) -> FacadeScope {
102    SCOPED_APPS.with(|apps| apps.borrow_mut().push(app));
103    FacadeScope { _not_send: PhantomData }
104}
105
106/// The innermost application scoped to this thread, if any.
107pub fn scoped_facade_application() -> Option<Arc<Application>> {
108    SCOPED_APPS.with(|apps| apps.borrow().last().cloned())
109}
110
111/// Install the application that facades resolve through.
112///
113/// Called once during bootstrap. Calling it again replaces the binding, which
114/// is what lets a test harness stand up a fresh application per test.
115pub fn set_facade_application(app: Arc<Application>) {
116    *FACADE_APP.write().expect("facade application lock poisoned") = Some(app);
117}
118
119/// The application facades resolve through, if one is installed.
120///
121/// A [scope](scope_facade_application) on this thread wins over the
122/// process-wide one, which is what lets a test have its own.
123pub fn try_facade_application() -> Option<Arc<Application>> {
124    if let Some(scoped) = scoped_facade_application() {
125        return Some(scoped);
126    }
127    FACADE_APP.read().expect("facade application lock poisoned").clone()
128}
129
130/// The application facades resolve through.
131///
132/// # Panics
133///
134/// If no application has been installed. That is a bootstrap bug — a facade
135/// was used before (or without) [`set_facade_application`] — so it fails loudly
136/// rather than returning an error every call site would have to unwrap.
137pub fn facade_application() -> Arc<Application> {
138    try_facade_application().expect(
139        "no application is bound to the facades — call `set_facade_application` during bootstrap \
140         (or use the service directly instead of the facade)",
141    )
142}
143
144/// Forget the installed application. Mainly for tests that assert on the
145/// unbound behaviour.
146pub fn clear_facade_application() {
147    *FACADE_APP.write().expect("facade application lock poisoned") = None;
148}
149
150/// A static proxy onto a container-resolved service.
151///
152/// Implementors are zero-sized marker types; all the behaviour lives in the
153/// [`Accessor`](Facade::Accessor) they resolve.
154pub trait Facade {
155    /// The service this facade forwards to.
156    type Accessor: Send + Sync + 'static;
157
158    /// Resolve the underlying service.
159    ///
160    /// # Panics
161    ///
162    /// If no application is installed, or the accessor is not bound in it.
163    /// Both are configuration bugs: a facade whose service was never registered
164    /// can never work, so it says so rather than propagating an error into
165    /// every caller.
166    fn instance() -> Arc<Self::Accessor> {
167        let app = facade_application();
168        match app.resolve::<Self::Accessor>() {
169            Ok(service) => service,
170            Err(e) => panic!(
171                "the `{}` facade could not resolve `{}`: {e}",
172                type_name::<Self>(),
173                type_name::<Self::Accessor>()
174            ),
175        }
176    }
177
178    /// Resolve the underlying service, or `None` if the application or the
179    /// binding is missing. For code that must degrade rather than panic.
180    fn try_instance() -> Option<Arc<Self::Accessor>> {
181        try_facade_application()?.try_resolve::<Self::Accessor>()
182    }
183}
184
185/// Declare a facade type and its [`Facade`] impl.
186///
187/// ```
188/// # use rainier_container::{facade, Application, Facade};
189/// # use std::sync::Arc;
190/// pub struct Clock {
191///     pub now: u64,
192/// }
193///
194/// facade!(
195///     /// Reads the application clock.
196///     Time => Clock
197/// );
198///
199/// # fn main() {
200/// let app = Application::new(".");
201/// app.instance(Clock { now: 1 });
202/// rainier_container::set_facade_application(Arc::new(app));
203///
204/// assert_eq!(Time::instance().now, 1);
205/// # rainier_container::clear_facade_application();
206/// # }
207/// ```
208#[macro_export]
209macro_rules! facade {
210    ($(#[$meta:meta])* $name:ident => $accessor:ty) => {
211        $(#[$meta])*
212        #[doc = ""]
213        #[doc = concat!("A [`Facade`](", stringify!($crate), "::Facade) over [`", stringify!($accessor), "`].")]
214        #[derive(Debug, Clone, Copy, Default)]
215        pub struct $name;
216
217        impl $crate::Facade for $name {
218            type Accessor = $accessor;
219        }
220    };
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::Container;
227
228    struct Clock(u64);
229    struct Missing;
230
231    struct Time;
232    impl Facade for Time {
233        type Accessor = Clock;
234    }
235
236    struct Nowhere;
237    impl Facade for Nowhere {
238        type Accessor = Missing;
239    }
240
241    // The facade application is process-global, so these tests must not run
242    // concurrently with one another.
243    static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
244
245    fn with_app(build: impl FnOnce(&Application)) -> std::sync::MutexGuard<'static, ()> {
246        let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
247        let app = Application::new(".");
248        build(&app);
249        set_facade_application(Arc::new(app));
250        guard
251    }
252
253    #[test]
254    fn a_facade_resolves_through_the_container() {
255        let _guard = with_app(|app| app.instance(Clock(5)));
256        assert_eq!(Time::instance().0, 5);
257        clear_facade_application();
258    }
259
260    #[test]
261    fn rebinding_the_accessor_swaps_what_the_facade_sees() {
262        let _guard = with_app(|app| app.instance(Clock(1)));
263        assert_eq!(Time::instance().0, 1);
264
265        // No `swap` API needed: the facade re-resolves on every call.
266        facade_application().instance(Clock(2));
267        assert_eq!(Time::instance().0, 2);
268        clear_facade_application();
269    }
270
271    #[test]
272    fn try_instance_is_none_for_an_unbound_accessor() {
273        let _guard = with_app(|_| {});
274        assert!(Nowhere::try_instance().is_none());
275        clear_facade_application();
276    }
277
278    #[test]
279    fn try_instance_is_none_with_no_application() {
280        let _guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
281        clear_facade_application();
282        assert!(Time::try_instance().is_none());
283    }
284
285    #[test]
286    #[should_panic(expected = "could not resolve")]
287    fn instance_panics_for_an_unbound_accessor() {
288        let _guard = with_app(|_| {});
289        let _ = Nowhere::instance();
290    }
291
292    #[test]
293    fn a_transient_binding_is_re_resolved_each_call() {
294        let _guard = with_app(|app| {
295            app.bind(|_: &Container| Ok(Clock(7)));
296        });
297        let a = Time::instance();
298        let b = Time::instance();
299        assert_eq!(a.0, b.0);
300        assert!(!Arc::ptr_eq(&a, &b));
301        clear_facade_application();
302    }
303
304    #[test]
305    fn a_scope_wins_over_the_process_wide_application() {
306        // The whole point: a test can have its own container without racing
307        // whatever else installed one globally.
308        let global = Arc::new(Application::new("global"));
309        let scoped = Arc::new(Application::new("scoped"));
310
311        set_facade_application(Arc::clone(&global));
312        assert_eq!(facade_application().base_path(), global.base_path());
313
314        {
315            let _scope = scope_facade_application(Arc::clone(&scoped));
316            assert_eq!(facade_application().base_path(), scoped.base_path());
317        }
318
319        assert_eq!(facade_application().base_path(), global.base_path(), "the scope is undone");
320        clear_facade_application();
321    }
322
323    #[test]
324    fn scopes_nest_and_unwind_in_order() {
325        let outer = Arc::new(Application::new("outer"));
326        let inner = Arc::new(Application::new("inner"));
327
328        let _outer = scope_facade_application(Arc::clone(&outer));
329        {
330            let _inner = scope_facade_application(Arc::clone(&inner));
331            assert_eq!(facade_application().base_path(), inner.base_path());
332        }
333        assert_eq!(facade_application().base_path(), outer.base_path());
334    }
335
336    #[test]
337    fn a_scope_that_panicked_does_not_leak_into_the_next_test() {
338        // `Drop` runs while unwinding, which is why the scope is a guard and
339        // not a pair of calls.
340        let scoped = Arc::new(Application::new("panicky"));
341
342        let outcome = std::panic::catch_unwind(|| {
343            let _scope = scope_facade_application(Arc::clone(&scoped));
344            panic!("as a test would");
345        });
346
347        assert!(outcome.is_err());
348        assert!(scoped_facade_application().is_none(), "the stack should be empty again");
349    }
350}