use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
pub type LoadHook = Arc<dyn Fn() + Send + Sync + 'static>;
#[derive(Default)]
struct HookState {
callbacks: Vec<LoadHook>,
ran: bool,
}
static LOAD_HOOKS: Lazy<Mutex<HashMap<String, HookState>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
pub fn on_load<F>(name: &str, callback: F)
where
F: Fn() + Send + Sync + 'static,
{
let callback: LoadHook = Arc::new(callback);
let should_run_immediately = {
let mut hooks = LOAD_HOOKS.lock();
let state = hooks.entry(name.to_owned()).or_default();
if state.ran {
true
} else {
state.callbacks.push(Arc::clone(&callback));
false
}
};
if should_run_immediately {
callback();
}
}
pub fn run_load_hooks(name: &str) {
let callbacks = {
let mut hooks = LOAD_HOOKS.lock();
let state = hooks.entry(name.to_owned()).or_default();
if state.ran {
return;
}
state.ran = true;
std::mem::take(&mut state.callbacks)
};
for callback in callbacks {
callback();
}
}
#[cfg(test)]
pub(crate) fn reset_hooks() {
LOAD_HOOKS.lock().clear();
}
#[cfg(test)]
mod tests {
use super::{on_load, reset_hooks, run_load_hooks};
use parking_lot::Mutex;
use std::sync::{Arc, LazyLock, Mutex as StdMutex};
static TEST_LOCK: LazyLock<StdMutex<()>> = LazyLock::new(|| StdMutex::new(()));
#[test]
fn registered_callbacks_run_when_hook_is_loaded() {
let _guard = TEST_LOCK.lock().expect("test lock should not be poisoned");
reset_hooks();
let calls = Arc::new(Mutex::new(Vec::new()));
let observed = Arc::clone(&calls);
on_load("active_record", move || observed.lock().push("ran"));
run_load_hooks("active_record");
assert_eq!(&*calls.lock(), &["ran"]);
}
#[test]
fn multiple_callbacks_run_in_registration_order() {
let _guard = TEST_LOCK.lock().expect("test lock should not be poisoned");
reset_hooks();
let calls = Arc::new(Mutex::new(Vec::new()));
let first = Arc::clone(&calls);
let second = Arc::clone(&calls);
on_load("action_controller", move || first.lock().push("first"));
on_load("action_controller", move || second.lock().push("second"));
run_load_hooks("action_controller");
assert_eq!(&*calls.lock(), &["first", "second"]);
}
#[test]
fn running_the_same_hook_twice_is_a_no_op() {
let _guard = TEST_LOCK.lock().expect("test lock should not be poisoned");
reset_hooks();
let calls = Arc::new(Mutex::new(0usize));
let observed = Arc::clone(&calls);
on_load("action_mailer", move || *observed.lock() += 1);
run_load_hooks("action_mailer");
run_load_hooks("action_mailer");
assert_eq!(*calls.lock(), 1);
}
#[test]
fn registering_after_a_hook_has_run_executes_immediately() {
let _guard = TEST_LOCK.lock().expect("test lock should not be poisoned");
reset_hooks();
let calls = Arc::new(Mutex::new(Vec::new()));
run_load_hooks("active_job");
let observed = Arc::clone(&calls);
on_load("active_job", move || observed.lock().push("immediate"));
assert_eq!(&*calls.lock(), &["immediate"]);
}
#[test]
fn different_hooks_do_not_interfere_with_each_other() {
let _guard = TEST_LOCK.lock().expect("test lock should not be poisoned");
reset_hooks();
let calls = Arc::new(Mutex::new(Vec::new()));
let record = Arc::clone(&calls);
let view = Arc::clone(&calls);
on_load("active_record", move || record.lock().push("record"));
on_load("action_view", move || view.lock().push("view"));
run_load_hooks("action_view");
assert_eq!(&*calls.lock(), &["view"]);
}
#[test]
fn running_an_unknown_hook_is_a_no_op() {
let _guard = TEST_LOCK.lock().expect("test lock should not be poisoned");
reset_hooks();
run_load_hooks("unknown");
run_load_hooks("unknown");
}
}