rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
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,
        }
    }
}

/// Sent at the beginning of Model.__init__.
pub static PRE_INIT: LazyLock<ModelSignal<ModelSignalArgs>> =
    LazyLock::new(|| ModelSignal::new("pre_init"));
/// Sent at the end of Model.__init__.
pub static POST_INIT: LazyLock<ModelSignal<ModelSignalArgs>> =
    LazyLock::new(|| ModelSignal::new("post_init"));
/// Sent before Model.save().
pub static PRE_SAVE: LazyLock<ModelSignal<ModelSignalArgs>> =
    LazyLock::new(|| ModelSignal::new("pre_save"));
/// Sent after Model.save().
pub static POST_SAVE: LazyLock<ModelSignal<ModelSignalArgs>> =
    LazyLock::new(|| ModelSignal::new("post_save"));
/// Sent before Model.delete().
pub static PRE_DELETE: LazyLock<ModelSignal<ModelSignalArgs>> =
    LazyLock::new(|| ModelSignal::new("pre_delete"));
/// Sent after Model.delete().
pub static POST_DELETE: LazyLock<ModelSignal<ModelSignalArgs>> =
    LazyLock::new(|| ModelSignal::new("post_delete"));
/// Sent when a ManyToManyField is changed.
pub static M2M_CHANGED: LazyLock<ModelSignal<ModelSignalArgs>> =
    LazyLock::new(|| ModelSignal::new("m2m_changed"));
/// Sent before migrate runs.
pub static PRE_MIGRATE: LazyLock<ModelSignal<ModelSignalArgs>> =
    LazyLock::new(|| ModelSignal::new("pre_migrate"));
/// Sent after migrate completes.
pub static POST_MIGRATE: LazyLock<ModelSignal<ModelSignalArgs>> =
    LazyLock::new(|| ModelSignal::new("post_migrate"));
/// Sent when a model class has been prepared.
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");
    }
}