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 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}