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}