Skip to main content

rainier_framework/
facades.rs

1//! The facades — static proxies onto container-resolved services.
2//!
3//! ```ignore
4//! use rainier_framework::facades::{Config, DB, Event, Queue};
5//!
6//! let name: String = Config::instance().get_or("app.name", "Rainier".into());
7//! Event::instance().dispatch(UserRegistered { id }).await?;
8//! Queue::instance().dispatch(SendWelcomeEmail { id }).await?;
9//! let posts: Vec<Post> = DB::instance().fetch_all(prepared).await?;
10//! ```
11//!
12//! Every call resolves through the container, so rebinding the accessor swaps
13//! what every facade call sees — which is how a test installs a fake without a
14//! separate `swap` mechanism.
15//!
16//! The convenience is real and so is the cost: a facade hides a dependency
17//! that a constructor argument would have made visible. Reach for one in
18//! application code and route closures; take the service as an argument in
19//! anything you intend to unit-test.
20
21use std::sync::Arc;
22
23use rainier_container::facade;
24use rainier_view::ViewEngine;
25
26/// The application's view engine, as a container-storable value.
27///
28/// A newtype rather than binding the engine directly, so an application can
29/// swap the default engine for something else without every `View::…` call
30/// site naming a different type.
31#[derive(Clone)]
32pub struct Views(pub Arc<dyn ViewEngine>);
33
34impl Views {
35    /// Wrap an engine.
36    pub fn new(engine: Arc<dyn ViewEngine>) -> Self {
37        Self(engine)
38    }
39
40    /// The engine.
41    pub fn engine(&self) -> &Arc<dyn ViewEngine> {
42        &self.0
43    }
44
45    /// Render a named view.
46    pub fn render(&self, name: &str, data: &serde_json::Value) -> rainier_support::Result<String> {
47        self.0.render(name, data)
48    }
49
50    /// Render a [`View`](rainier_view::View).
51    pub fn render_view(&self, view: &rainier_view::View) -> rainier_support::Result<String> {
52        self.0.render_view(view)
53    }
54}
55
56impl std::fmt::Debug for Views {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.write_str("Views(..)")
59    }
60}
61
62facade!(
63    /// The configuration repository.
64    Config => rainier_config::Config
65);
66
67facade!(
68    /// The event dispatcher — Rainier's hook bus.
69    Event => rainier_events::Dispatcher
70);
71
72facade!(
73    /// The database handle.
74    DB => rainier_database::Database
75);
76
77facade!(
78    /// The queue dispatcher.
79    Queue => rainier_queue::QueueManager
80);
81
82facade!(
83    /// The mailer.
84    Mail => rainier_mail::Mailer
85);
86
87facade!(
88    /// The view engine.
89    View => Views
90);
91
92facade!(
93    /// URL generation for named routes.
94    Url => rainier_routing::UrlGenerator
95);
96
97facade!(
98    /// The middleware registry.
99    Middleware => rainier_middleware::MiddlewareRegistry
100);
101
102facade!(
103    /// Encryption and signing.
104    Crypt => rainier_crypt::Encryption
105);
106
107facade!(
108    /// Notifications.
109    Notify => rainier_notify::Notifier
110);
111
112facade!(
113    /// Broadcasting to WebSocket channels.
114    Broadcast => rainier_broadcast::Broadcasting
115);
116
117facade!(
118    /// The cache.
119    Cache => rainier_cache::CacheManager
120);
121
122facade!(
123    /// File storage.
124    Storage => rainier_filesystem::Storage
125);
126
127facade!(
128    /// The session **store** and its settings.
129    ///
130    /// Not a request's session — a facade is process-global and a session
131    /// belongs to one request, so with two requests in flight there is
132    /// nothing honest for `Session::instance().get(..)` to return. Reach a
133    /// request's own bag with
134    /// [`request.session()`](rainier_session::SessionRequestExt::session).
135    ///
136    /// This is for what genuinely is application-wide: reading or destroying
137    /// a session by id, and collecting expired ones.
138    Session => rainier_session::SessionManager
139);
140
141// There is deliberately **no** `Auth` facade here. `AuthManager<U>` is generic
142// over the application's user model, and a facade is a concrete type — so the
143// application declares its own in one line:
144//
145// ```ignore
146// rainier_framework::facade!(Auth => rainier_framework::auth::AuthManager<app::models::User>);
147// ```
148//
149// See the crate docs for why the manager is generic rather than erasing the
150// user behind `dyn Authenticatable`.
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use rainier_container::{
156        clear_facade_application, set_facade_application, Application, Facade,
157    };
158    use rainier_view::MemoryEngine;
159
160    // The facade application is process-global, so these must not interleave.
161    static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
162
163    fn install(build: impl FnOnce(&Application)) -> std::sync::MutexGuard<'static, ()> {
164        let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
165        let app = Application::new(".");
166        build(&app);
167        set_facade_application(Arc::new(app));
168        guard
169    }
170
171    #[test]
172    fn the_config_facade_reads_through_the_container() {
173        let _guard = install(|app| {
174            let config = rainier_config::Config::new();
175            config.set("app.name", "Rainier").unwrap();
176            app.instance(config);
177        });
178
179        assert_eq!(Config::instance().string("app.name").as_deref(), Some("Rainier"));
180        clear_facade_application();
181    }
182
183    #[test]
184    fn the_view_facade_renders() {
185        let _guard = install(|app| {
186            app.instance(Views::new(Arc::new(
187                MemoryEngine::new().with("greeting", "Hi {{ name }}"),
188            )));
189        });
190
191        let rendered =
192            View::instance().render("greeting", &serde_json::json!({ "name": "Ada" })).unwrap();
193        assert_eq!(rendered, "Hi Ada");
194        clear_facade_application();
195    }
196
197    #[test]
198    fn rebinding_swaps_what_every_call_sees() {
199        let _guard = install(|app| {
200            app.instance(Views::new(Arc::new(MemoryEngine::new().with("t", "first"))));
201        });
202        assert_eq!(View::instance().render("t", &serde_json::json!({})).unwrap(), "first");
203
204        rainier_container::facade_application()
205            .instance(Views::new(Arc::new(MemoryEngine::new().with("t", "second"))));
206        assert_eq!(View::instance().render("t", &serde_json::json!({})).unwrap(), "second");
207
208        clear_facade_application();
209    }
210
211    #[test]
212    fn an_unbound_facade_reads_as_none_rather_than_panicking_through_try() {
213        let _guard = install(|_| {});
214        assert!(DB::try_instance().is_none());
215        clear_facade_application();
216    }
217}