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 crate::application::Application;
39
40/// The application backing every facade in this process.
41static FACADE_APP: RwLock<Option<Arc<Application>>> = RwLock::new(None);
42
43/// Install the application that facades resolve through.
44///
45/// Called once during bootstrap. Calling it again replaces the binding, which
46/// is what lets a test harness stand up a fresh application per test.
47pub fn set_facade_application(app: Arc<Application>) {
48 *FACADE_APP.write().expect("facade application lock poisoned") = Some(app);
49}
50
51/// The application facades resolve through, if one is installed.
52pub fn try_facade_application() -> Option<Arc<Application>> {
53 FACADE_APP.read().expect("facade application lock poisoned").clone()
54}
55
56/// The application facades resolve through.
57///
58/// # Panics
59///
60/// If no application has been installed. That is a bootstrap bug — a facade
61/// was used before (or without) [`set_facade_application`] — so it fails loudly
62/// rather than returning an error every call site would have to unwrap.
63pub fn facade_application() -> Arc<Application> {
64 try_facade_application().expect(
65 "no application is bound to the facades — call `set_facade_application` during bootstrap \
66 (or use the service directly instead of the facade)",
67 )
68}
69
70/// Forget the installed application. Mainly for tests that assert on the
71/// unbound behaviour.
72pub fn clear_facade_application() {
73 *FACADE_APP.write().expect("facade application lock poisoned") = None;
74}
75
76/// A static proxy onto a container-resolved service.
77///
78/// Implementors are zero-sized marker types; all the behaviour lives in the
79/// [`Accessor`](Facade::Accessor) they resolve.
80pub trait Facade {
81 /// The service this facade forwards to.
82 type Accessor: Send + Sync + 'static;
83
84 /// Resolve the underlying service.
85 ///
86 /// # Panics
87 ///
88 /// If no application is installed, or the accessor is not bound in it.
89 /// Both are configuration bugs: a facade whose service was never registered
90 /// can never work, so it says so rather than propagating an error into
91 /// every caller.
92 fn instance() -> Arc<Self::Accessor> {
93 let app = facade_application();
94 match app.resolve::<Self::Accessor>() {
95 Ok(service) => service,
96 Err(e) => panic!(
97 "the `{}` facade could not resolve `{}`: {e}",
98 type_name::<Self>(),
99 type_name::<Self::Accessor>()
100 ),
101 }
102 }
103
104 /// Resolve the underlying service, or `None` if the application or the
105 /// binding is missing. For code that must degrade rather than panic.
106 fn try_instance() -> Option<Arc<Self::Accessor>> {
107 try_facade_application()?.try_resolve::<Self::Accessor>()
108 }
109}
110
111/// Declare a facade type and its [`Facade`] impl.
112///
113/// ```
114/// # use rainier_container::{facade, Application, Facade};
115/// # use std::sync::Arc;
116/// pub struct Clock {
117/// pub now: u64,
118/// }
119///
120/// facade!(
121/// /// Reads the application clock.
122/// Time => Clock
123/// );
124///
125/// # fn main() {
126/// let app = Application::new(".");
127/// app.instance(Clock { now: 1 });
128/// rainier_container::set_facade_application(Arc::new(app));
129///
130/// assert_eq!(Time::instance().now, 1);
131/// # rainier_container::clear_facade_application();
132/// # }
133/// ```
134#[macro_export]
135macro_rules! facade {
136 ($(#[$meta:meta])* $name:ident => $accessor:ty) => {
137 $(#[$meta])*
138 #[doc = ""]
139 #[doc = concat!("A [`Facade`](", stringify!($crate), "::Facade) over [`", stringify!($accessor), "`].")]
140 #[derive(Debug, Clone, Copy, Default)]
141 pub struct $name;
142
143 impl $crate::Facade for $name {
144 type Accessor = $accessor;
145 }
146 };
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::Container;
153
154 struct Clock(u64);
155 struct Missing;
156
157 struct Time;
158 impl Facade for Time {
159 type Accessor = Clock;
160 }
161
162 struct Nowhere;
163 impl Facade for Nowhere {
164 type Accessor = Missing;
165 }
166
167 // The facade application is process-global, so these tests must not run
168 // concurrently with one another.
169 static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
170
171 fn with_app(build: impl FnOnce(&Application)) -> std::sync::MutexGuard<'static, ()> {
172 let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
173 let app = Application::new(".");
174 build(&app);
175 set_facade_application(Arc::new(app));
176 guard
177 }
178
179 #[test]
180 fn a_facade_resolves_through_the_container() {
181 let _guard = with_app(|app| app.instance(Clock(5)));
182 assert_eq!(Time::instance().0, 5);
183 clear_facade_application();
184 }
185
186 #[test]
187 fn rebinding_the_accessor_swaps_what_the_facade_sees() {
188 let _guard = with_app(|app| app.instance(Clock(1)));
189 assert_eq!(Time::instance().0, 1);
190
191 // No `swap` API needed: the facade re-resolves on every call.
192 facade_application().instance(Clock(2));
193 assert_eq!(Time::instance().0, 2);
194 clear_facade_application();
195 }
196
197 #[test]
198 fn try_instance_is_none_for_an_unbound_accessor() {
199 let _guard = with_app(|_| {});
200 assert!(Nowhere::try_instance().is_none());
201 clear_facade_application();
202 }
203
204 #[test]
205 fn try_instance_is_none_with_no_application() {
206 let _guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
207 clear_facade_application();
208 assert!(Time::try_instance().is_none());
209 }
210
211 #[test]
212 #[should_panic(expected = "could not resolve")]
213 fn instance_panics_for_an_unbound_accessor() {
214 let _guard = with_app(|_| {});
215 let _ = Nowhere::instance();
216 }
217
218 #[test]
219 fn a_transient_binding_is_re_resolved_each_call() {
220 let _guard = with_app(|app| {
221 app.bind(|_: &Container| Ok(Clock(7)));
222 });
223 let a = Time::instance();
224 let b = Time::instance();
225 assert_eq!(a.0, b.0);
226 assert!(!Arc::ptr_eq(&a, &b));
227 clear_facade_application();
228 }
229}