use std::any::type_name;
use std::sync::{Arc, RwLock};
use std::cell::RefCell;
use std::marker::PhantomData;
use crate::application::Application;
static FACADE_APP: RwLock<Option<Arc<Application>>> = RwLock::new(None);
thread_local! {
static SCOPED_APPS: RefCell<Vec<Arc<Application>>> = const { RefCell::new(Vec::new()) };
}
tokio::task_local! {
static TASK_APP: Arc<Application>;
}
#[must_use = "the scope ends the moment this is dropped"]
pub struct FacadeScope {
_not_send: PhantomData<*const ()>,
}
impl Drop for FacadeScope {
fn drop(&mut self) {
SCOPED_APPS.with(|apps| {
apps.borrow_mut().pop();
});
}
}
pub fn scope_facade_application(app: Arc<Application>) -> FacadeScope {
SCOPED_APPS.with(|apps| apps.borrow_mut().push(app));
FacadeScope { _not_send: PhantomData }
}
pub fn scoped_facade_application() -> Option<Arc<Application>> {
SCOPED_APPS.with(|apps| apps.borrow().last().cloned())
}
pub async fn with_facade_application<F>(app: Arc<Application>, future: F) -> F::Output
where
F: std::future::Future,
{
TASK_APP.scope(app, future).await
}
pub fn task_facade_application() -> Option<Arc<Application>> {
TASK_APP.try_with(Arc::clone).ok()
}
pub fn spawn_with_facades<F>(future: F) -> tokio::task::JoinHandle<F::Output>
where
F: std::future::Future + Send + 'static,
F::Output: Send + 'static,
{
match try_facade_application() {
Some(app) => tokio::spawn(with_facade_application(app, future)),
None => tokio::spawn(future),
}
}
pub fn set_facade_application(app: Arc<Application>) {
*FACADE_APP.write().expect("facade application lock poisoned") = Some(app);
}
pub fn try_facade_application() -> Option<Arc<Application>> {
if let Some(scoped) = task_facade_application() {
return Some(scoped);
}
if let Some(scoped) = scoped_facade_application() {
return Some(scoped);
}
FACADE_APP.read().expect("facade application lock poisoned").clone()
}
pub fn facade_application() -> Arc<Application> {
try_facade_application().expect(
"no application is bound to the facades — call `set_facade_application` during bootstrap \
(or use the service directly instead of the facade)",
)
}
pub fn clear_facade_application() {
*FACADE_APP.write().expect("facade application lock poisoned") = None;
}
pub trait Facade {
type Accessor: Send + Sync + 'static;
fn instance() -> Arc<Self::Accessor> {
let app = facade_application();
match app.resolve::<Self::Accessor>() {
Ok(service) => service,
Err(e) => panic!(
"the `{}` facade could not resolve `{}`: {e}",
type_name::<Self>(),
type_name::<Self::Accessor>()
),
}
}
fn try_instance() -> Option<Arc<Self::Accessor>> {
try_facade_application()?.try_resolve::<Self::Accessor>()
}
}
#[macro_export]
macro_rules! facade {
($(#[$meta:meta])* $name:ident => $accessor:ty) => {
$(#[$meta])*
#[doc = ""]
#[doc = concat!("A [`Facade`](", stringify!($crate), "::Facade) over [`", stringify!($accessor), "`].")]
#[derive(Debug, Clone, Copy, Default)]
pub struct $name;
impl $crate::Facade for $name {
type Accessor = $accessor;
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Container;
struct Clock(u64);
struct Missing;
struct Time;
impl Facade for Time {
type Accessor = Clock;
}
struct Nowhere;
impl Facade for Nowhere {
type Accessor = Missing;
}
static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn with_app(build: impl FnOnce(&Application)) -> std::sync::MutexGuard<'static, ()> {
let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let app = Application::new(".");
build(&app);
set_facade_application(Arc::new(app));
guard
}
#[test]
fn a_facade_resolves_through_the_container() {
let _guard = with_app(|app| app.instance(Clock(5)));
assert_eq!(Time::instance().0, 5);
clear_facade_application();
}
#[test]
fn rebinding_the_accessor_swaps_what_the_facade_sees() {
let _guard = with_app(|app| app.instance(Clock(1)));
assert_eq!(Time::instance().0, 1);
facade_application().instance(Clock(2));
assert_eq!(Time::instance().0, 2);
clear_facade_application();
}
#[test]
fn try_instance_is_none_for_an_unbound_accessor() {
let _guard = with_app(|_| {});
assert!(Nowhere::try_instance().is_none());
clear_facade_application();
}
#[test]
fn try_instance_is_none_with_no_application() {
let _guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
clear_facade_application();
assert!(Time::try_instance().is_none());
}
#[test]
#[should_panic(expected = "could not resolve")]
fn instance_panics_for_an_unbound_accessor() {
let _guard = with_app(|_| {});
let _ = Nowhere::instance();
}
#[test]
fn a_transient_binding_is_re_resolved_each_call() {
let _guard = with_app(|app| {
app.bind(|_: &Container| Ok(Clock(7)));
});
let a = Time::instance();
let b = Time::instance();
assert_eq!(a.0, b.0);
assert!(!Arc::ptr_eq(&a, &b));
clear_facade_application();
}
#[test]
fn a_scope_wins_over_the_process_wide_application() {
let _guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let global = Arc::new(Application::new("global"));
let scoped = Arc::new(Application::new("scoped"));
set_facade_application(Arc::clone(&global));
assert_eq!(facade_application().base_path(), global.base_path());
{
let _scope = scope_facade_application(Arc::clone(&scoped));
assert_eq!(facade_application().base_path(), scoped.base_path());
}
assert_eq!(facade_application().base_path(), global.base_path(), "the scope is undone");
clear_facade_application();
}
#[test]
fn scopes_nest_and_unwind_in_order() {
let outer = Arc::new(Application::new("outer"));
let inner = Arc::new(Application::new("inner"));
let _outer = scope_facade_application(Arc::clone(&outer));
{
let _inner = scope_facade_application(Arc::clone(&inner));
assert_eq!(facade_application().base_path(), inner.base_path());
}
assert_eq!(facade_application().base_path(), outer.base_path());
}
#[test]
fn a_scope_that_panicked_does_not_leak_into_the_next_test() {
let scoped = Arc::new(Application::new("panicky"));
let outcome = std::panic::catch_unwind(|| {
let _scope = scope_facade_application(Arc::clone(&scoped));
panic!("as a test would");
});
assert!(outcome.is_err());
assert!(scoped_facade_application().is_none(), "the stack should be empty again");
}
}