use std::sync::LazyLock;
use crate::dispatch::{HandlerId, Signal};
#[derive(Clone)]
pub struct ModelSignal<T: Clone + Send + Sync + 'static> {
name: &'static str,
signal: Signal<T>,
}
impl<T: Clone + Send + Sync + 'static> ModelSignal<T> {
#[must_use]
pub fn new(name: &'static str) -> Self {
Self {
name,
signal: Signal::new(),
}
}
#[must_use]
pub fn name(&self) -> &'static str {
self.name
}
pub fn connect<F>(&self, handler: F, dispatch_uid: Option<String>) -> HandlerId
where
F: Fn(T) + Send + Sync + 'static,
{
self.signal.connect(handler, dispatch_uid)
}
pub fn disconnect(&self, id: HandlerId) -> bool {
self.signal.disconnect(id)
}
pub fn send(&self, args: T) {
self.signal.send(args);
}
#[must_use]
pub fn has_listeners(&self) -> bool {
self.signal.has_listeners()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelSignalArgs {
pub model_name: String,
pub instance_id: Option<String>,
pub using: String,
pub created: bool,
}
impl ModelSignalArgs {
#[must_use]
pub fn new(
model_name: impl Into<String>,
instance_id: Option<String>,
using: impl Into<String>,
created: bool,
) -> Self {
Self {
model_name: model_name.into(),
instance_id,
using: using.into(),
created,
}
}
}
pub static PRE_INIT: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("pre_init"));
pub static POST_INIT: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("post_init"));
pub static PRE_SAVE: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("pre_save"));
pub static POST_SAVE: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("post_save"));
pub static PRE_DELETE: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("pre_delete"));
pub static POST_DELETE: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("post_delete"));
pub static M2M_CHANGED: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("m2m_changed"));
pub static PRE_MIGRATE: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("pre_migrate"));
pub static POST_MIGRATE: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("post_migrate"));
pub static CLASS_PREPARED: LazyLock<ModelSignal<ModelSignalArgs>> =
LazyLock::new(|| ModelSignal::new("class_prepared"));
fn connect_model_signal<F>(signal: &ModelSignal<ModelSignalArgs>, handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
signal.connect(handler, None)
}
pub fn connect_pre_init<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&PRE_INIT, handler)
}
pub fn connect_post_init<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&POST_INIT, handler)
}
pub fn connect_pre_save<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&PRE_SAVE, handler)
}
pub fn connect_post_save<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&POST_SAVE, handler)
}
pub fn connect_pre_delete<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&PRE_DELETE, handler)
}
pub fn connect_post_delete<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&POST_DELETE, handler)
}
pub fn connect_m2m_changed<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&M2M_CHANGED, handler)
}
pub fn connect_pre_migrate<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&PRE_MIGRATE, handler)
}
pub fn connect_post_migrate<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&POST_MIGRATE, handler)
}
pub fn connect_class_prepared<F>(handler: F) -> HandlerId
where
F: Fn(ModelSignalArgs) + Send + Sync + 'static,
{
connect_model_signal(&CLASS_PREPARED, handler)
}
#[cfg(test)]
mod tests {
use super::{
CLASS_PREPARED, M2M_CHANGED, ModelSignal, ModelSignalArgs, POST_DELETE, POST_MIGRATE,
POST_SAVE, PRE_DELETE, PRE_INIT, PRE_MIGRATE, PRE_SAVE,
};
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
#[test]
fn test_pre_save_signal_exists() {
assert_eq!(PRE_SAVE.name(), "pre_save");
}
#[test]
fn test_post_save_signal_exists() {
assert_eq!(POST_SAVE.name(), "post_save");
}
#[test]
fn test_pre_delete_signal_exists() {
assert_eq!(PRE_DELETE.name(), "pre_delete");
}
#[test]
fn test_post_delete_signal_exists() {
assert_eq!(POST_DELETE.name(), "post_delete");
}
#[test]
fn test_m2m_changed_signal_exists() {
assert_eq!(M2M_CHANGED.name(), "m2m_changed");
}
#[test]
fn test_pre_migrate_signal_exists() {
assert_eq!(PRE_MIGRATE.name(), "pre_migrate");
}
#[test]
fn test_signal_connect_and_send() {
let signal = ModelSignal::new("test_model_signal");
let received = Arc::new(Mutex::new(Vec::new()));
let seen = Arc::clone(&received);
let expected = ModelSignalArgs::new("Article", Some("42".to_string()), "default", true);
let handler_id = signal.connect(
move |args| {
seen.lock().expect("lock should succeed").push(args);
},
None,
);
signal.send(expected.clone());
let dispatched = received.lock().expect("lock should succeed").clone();
assert_eq!(dispatched, vec![expected]);
assert!(signal.disconnect(handler_id));
}
#[test]
fn test_model_signal_args_creation() {
let args = ModelSignalArgs::new("Widget", Some("7".to_string()), "replica", false);
assert_eq!(args.model_name, "Widget");
assert_eq!(args.instance_id.as_deref(), Some("7"));
assert_eq!(args.using, "replica");
assert!(!args.created);
}
#[test]
fn test_all_signals_have_distinct_names() {
let names = [
PRE_INIT.name(),
super::POST_INIT.name(),
PRE_SAVE.name(),
POST_SAVE.name(),
PRE_DELETE.name(),
POST_DELETE.name(),
M2M_CHANGED.name(),
PRE_MIGRATE.name(),
POST_MIGRATE.name(),
CLASS_PREPARED.name(),
];
let unique_names: HashSet<_> = names.into_iter().collect();
assert_eq!(unique_names.len(), names.len());
}
#[test]
fn test_class_prepared_signal_exists() {
assert_eq!(CLASS_PREPARED.name(), "class_prepared");
}
}