rustrails-record 0.1.2

ORM layer (ActiveRecord equivalent)
Documentation
use rustrails_model::callbacks::ModelEvent;
use rustrails_support::callbacks::{Callback, CallbackChain, CallbackResult};

use crate::Record;

/// Trait for records that expose persistence-related callback hooks.
pub trait RecordCallbacks: Record {
    /// Returns callbacks that run before save operations.
    fn before_save_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
        &[]
    }

    /// Returns callbacks that run after save operations.
    fn after_save_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
        &[]
    }

    /// Returns callbacks that run before create operations.
    fn before_create_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
        &[]
    }

    /// Returns callbacks that run after create operations.
    fn after_create_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
        &[]
    }

    /// Returns callbacks that run before update operations.
    fn before_update_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
        &[]
    }

    /// Returns callbacks that run after update operations.
    fn after_update_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
        &[]
    }

    /// Returns callbacks that run before destroy operations.
    fn before_destroy_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
        &[]
    }

    /// Returns callbacks that run after destroy operations.
    fn after_destroy_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
        &[]
    }

    /// Returns the raw callback list for the given lifecycle event.
    #[must_use]
    fn callbacks_for(event: ModelEvent) -> &'static [fn(&mut Self) -> CallbackResult] {
        match event {
            ModelEvent::BeforeValidation | ModelEvent::AfterValidation => &[],
            ModelEvent::BeforeSave => Self::before_save_callbacks(),
            ModelEvent::AfterSave => Self::after_save_callbacks(),
            ModelEvent::BeforeCreate => Self::before_create_callbacks(),
            ModelEvent::AfterCreate => Self::after_create_callbacks(),
            ModelEvent::BeforeUpdate => Self::before_update_callbacks(),
            ModelEvent::AfterUpdate => Self::after_update_callbacks(),
            ModelEvent::BeforeDestroy => Self::before_destroy_callbacks(),
            ModelEvent::AfterDestroy => Self::after_destroy_callbacks(),
        }
    }

    /// Builds a callback chain for the requested lifecycle event.
    #[must_use]
    fn callback_chain(event: ModelEvent) -> CallbackChain<Self> {
        let mut chain = CallbackChain::new(event.as_str());
        chain.set_run_after_on_halt(false);

        for (index, callback) in Self::callbacks_for(event).iter().copied().enumerate() {
            let name = format!("{}_{}", event.as_str(), index);
            if is_after_event(event) {
                chain.add(Callback::after(name, callback));
            } else {
                chain.add(Callback::before(name, callback));
            }
        }

        chain
    }

    /// Runs the callbacks registered for the requested lifecycle event.
    fn run_callbacks(&mut self, event: ModelEvent) -> CallbackResult {
        Self::callback_chain(event).run(self, event.as_str())
    }
}

fn is_after_event(event: ModelEvent) -> bool {
    matches!(
        event,
        ModelEvent::AfterValidation
            | ModelEvent::AfterSave
            | ModelEvent::AfterCreate
            | ModelEvent::AfterUpdate
            | ModelEvent::AfterDestroy
    )
}

#[cfg(test)]
mod tests {
    use rustrails_model::callbacks::ModelEvent;
    use rustrails_support::callbacks::CallbackResult;

    use super::RecordCallbacks;
    use crate::base::test_support::TestUser;

    fn before_save(user: &mut TestUser) -> CallbackResult {
        user.name.push_str("-before-save");
        CallbackResult::Continue
    }

    fn after_save_first(user: &mut TestUser) -> CallbackResult {
        user.email.push('1');
        CallbackResult::Continue
    }

    fn after_save_second(user: &mut TestUser) -> CallbackResult {
        user.email.push('2');
        CallbackResult::Continue
    }

    fn before_update_halt(user: &mut TestUser) -> CallbackResult {
        user.name.push_str("-halted");
        CallbackResult::Halt
    }

    fn before_update_never_runs(user: &mut TestUser) -> CallbackResult {
        user.name.push_str("-second");
        CallbackResult::Continue
    }

    fn after_create(user: &mut TestUser) -> CallbackResult {
        user.email.push_str(".created");
        CallbackResult::Continue
    }

    fn after_destroy(user: &mut TestUser) -> CallbackResult {
        user.email.push_str(".destroyed");
        CallbackResult::Continue
    }

    impl RecordCallbacks for TestUser {
        fn before_save_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
            &[before_save]
        }

        fn after_save_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
            &[after_save_first, after_save_second]
        }

        fn before_update_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
            &[before_update_halt, before_update_never_runs]
        }

        fn after_create_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
            &[after_create]
        }

        fn after_destroy_callbacks() -> &'static [fn(&mut Self) -> CallbackResult] {
            &[after_destroy]
        }
    }

    #[test]
    fn before_save_callbacks_fire_for_before_save_event() {
        let mut user = TestUser::default();

        let result = user.run_callbacks(ModelEvent::BeforeSave);

        assert_eq!(result, CallbackResult::Continue);
        assert_eq!(user.name, "-before-save");
    }

    #[test]
    fn after_callbacks_run_in_reverse_order() {
        let mut user = TestUser::default();

        let result = user.run_callbacks(ModelEvent::AfterSave);

        assert_eq!(result, CallbackResult::Continue);
        assert_eq!(user.email, "12");
    }

    #[test]
    fn create_and_destroy_events_use_their_specific_callback_lists() {
        let mut user = TestUser::default();

        assert_eq!(
            user.run_callbacks(ModelEvent::AfterCreate),
            CallbackResult::Continue
        );
        assert_eq!(
            user.run_callbacks(ModelEvent::AfterDestroy),
            CallbackResult::Continue
        );
        assert_eq!(user.email, ".created.destroyed");
    }

    #[test]
    fn halting_before_update_stops_later_callbacks() {
        let mut user = TestUser::default();

        let result = user.run_callbacks(ModelEvent::BeforeUpdate);

        assert_eq!(result, CallbackResult::Halt);
        assert_eq!(user.name, "-halted");
    }

    #[test]
    fn validation_events_default_to_no_callbacks() {
        let mut user = TestUser::default();

        let result = user.run_callbacks(ModelEvent::BeforeValidation);

        assert_eq!(result, CallbackResult::Continue);
        assert!(user.name.is_empty());
        assert!(user.email.is_empty());
    }

    #[test]
    fn after_validation_defaults_to_no_callbacks() {
        let mut user = TestUser::default();

        let result = user.run_callbacks(ModelEvent::AfterValidation);

        assert_eq!(result, CallbackResult::Continue);
        assert!(user.name.is_empty());
        assert!(user.email.is_empty());
    }

    #[test]
    fn before_create_defaults_to_no_callbacks() {
        let mut user = TestUser::default();

        let result = user.run_callbacks(ModelEvent::BeforeCreate);

        assert_eq!(result, CallbackResult::Continue);
        assert!(user.name.is_empty());
        assert!(user.email.is_empty());
    }

    #[test]
    fn after_update_defaults_to_no_callbacks() {
        let mut user = TestUser::default();

        let result = user.run_callbacks(ModelEvent::AfterUpdate);

        assert_eq!(result, CallbackResult::Continue);
        assert!(user.name.is_empty());
        assert!(user.email.is_empty());
    }

    #[test]
    fn before_destroy_defaults_to_no_callbacks() {
        let mut user = TestUser::default();

        let result = user.run_callbacks(ModelEvent::BeforeDestroy);

        assert_eq!(result, CallbackResult::Continue);
        assert!(user.name.is_empty());
        assert!(user.email.is_empty());
    }

    #[test]
    fn callbacks_for_before_validation_is_empty() {
        assert!(
            <TestUser as RecordCallbacks>::callbacks_for(ModelEvent::BeforeValidation).is_empty()
        );
    }

    #[test]
    fn callbacks_for_after_validation_is_empty() {
        assert!(
            <TestUser as RecordCallbacks>::callbacks_for(ModelEvent::AfterValidation).is_empty()
        );
    }

    #[test]
    fn callbacks_for_before_save_returns_registered_callback() {
        assert_eq!(
            <TestUser as RecordCallbacks>::callbacks_for(ModelEvent::BeforeSave).len(),
            1
        );
    }

    #[test]
    fn callbacks_for_after_save_returns_registered_callbacks() {
        assert_eq!(
            <TestUser as RecordCallbacks>::callbacks_for(ModelEvent::AfterSave).len(),
            2
        );
    }

    #[test]
    fn callbacks_for_after_create_returns_registered_callback() {
        assert_eq!(
            <TestUser as RecordCallbacks>::callbacks_for(ModelEvent::AfterCreate).len(),
            1
        );
    }

    #[test]
    fn callbacks_for_after_destroy_returns_registered_callback() {
        assert_eq!(
            <TestUser as RecordCallbacks>::callbacks_for(ModelEvent::AfterDestroy).len(),
            1
        );
    }

    #[test]
    fn callback_chain_for_before_save_exposes_generated_before_metadata() {
        let chain = <TestUser as RecordCallbacks>::callback_chain(ModelEvent::BeforeSave);

        assert_eq!(chain.name(), "before_save");
        assert_eq!(chain.len(), 1);

        let debug = format!("{chain:?}");
        assert!(debug.contains("before_save_0"));
        assert!(debug.contains("kind: Before"));
        assert!(debug.contains("run_after_on_halt: false"));
    }

    #[test]
    fn callback_chain_for_after_save_exposes_generated_after_metadata() {
        let chain = <TestUser as RecordCallbacks>::callback_chain(ModelEvent::AfterSave);

        assert_eq!(chain.name(), "after_save");
        assert_eq!(chain.len(), 2);

        let debug = format!("{chain:?}");
        assert!(debug.contains("after_save_0"));
        assert!(debug.contains("after_save_1"));
        assert!(debug.contains("kind: After"));
    }

    #[test]
    fn callback_chain_for_empty_event_is_named_and_empty() {
        let chain = <TestUser as RecordCallbacks>::callback_chain(ModelEvent::BeforeCreate);

        assert_eq!(chain.name(), "before_create");
        assert!(chain.is_empty());
        assert_eq!(chain.len(), 0);
    }
}