hookable 0.1.1

A thread-safe hook system that allows registering and executing sync and async hooks.
Documentation
/// A thread-safe hook system that allows registering and executing sync and async hooks.
///
/// Hooks are functions that can be attached to named events and executed when those
/// events are triggered. This struct supports both synchronous and asynchronous hooks.
///
/// # Basic usage
/// ```
/// use std::time::Duration;
/// use tokio::time::sleep;
/// use hookable::Hookable;
/// let hookable = Hookable::new();
///
/// // Register a sync hook
/// hookable.hook("event", || println!("Sync hook executed"));
///
/// // Register an async hook
/// hookable.hook_async("event1", || Box::pin(async {
///    sleep(Duration::from_millis(200)).await;
/// }));
///
/// // Execute hooks
/// hookable.call("event").unwrap();
/// hookable.call_async("async_event").await.unwrap();
/// ```
///
/// - Each returns a result that should be handled.
///
/// # Async and sync
/// You can assign both async and sync to the same name. If you do that you will always have to call it using `call_async`.
/// ```
/// use std::time::Duration;
/// use tokio::time::sleep;
/// use hookable::Hookable;
/// let hookable = Hookable::new();
///
/// // Register a sync hook
/// hookable.hook("event", || println!("Sync hook executed"));
///
/// // Register an async hook
/// hookable.hook_async("event", || Box::pin(async {
///    sleep(Duration::from_millis(200)).await;
/// }));
///
/// hookable.call("event").unwrap(); // ❌ This won't work because one of the events has to be run using await. `HookableError::AsyncHookCalledSync` will be returned.
/// hookable.call_async("event").await.unwrap(); // ✅ This will work.
/// ```
pub mod errors;
mod hook;
mod hookable;

pub use hook::*;
pub use hookable::*;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::errors::HookableError;
    use hookable::Hookable;
    use std::sync::{Arc, Mutex};
    use std::time::Duration;
    use tokio::runtime::Runtime;
    use tokio::time::sleep;

    fn base_hooks() -> Hookable {
        let hooks = Hookable::new();
        hooks.hook("finishLoad", || {
            println!("finishLoad executed");
        });
        hooks
    }

    fn async_hooks() -> Hookable {
        let hooks = Hookable::new();
        hooks.hook_async("asyncEvent", || {
            Box::pin(async {
                sleep(Duration::from_millis(100)).await;
                println!("Async hook executed");
            })
        });
        hooks
    }

    #[test]
    fn test_basic_sync_hook() {
        let hooks = base_hooks();
        let result = hooks.call("finishLoad");
        assert!(result.is_ok());
    }

    #[test]
    fn test_hook_not_found() {
        let hooks = Hookable::new();
        let result = hooks.call("nonExistentHook");
        assert!(result.is_err());

        if let Err(e) = result {
            match e {
                HookableError::NoHookFound(name) => {
                    assert_eq!(name, "nonExistentHook");
                }
                _ => panic!("Expected NoHookFound error"),
            }
        }
    }

    #[test]
    fn test_multiple_sync_hooks() {
        let hooks = Hookable::new();
        let counter = Arc::new(Mutex::new(0));

        // Register multiple hooks for the same event
        let counter1 = counter.clone();
        hooks.hook("multiHook", move || {
            let mut count = counter1.lock().unwrap();
            *count += 1;
        });

        let counter2 = counter.clone();
        hooks.hook("multiHook", move || {
            let mut count = counter2.lock().unwrap();
            *count += 10;
        });

        let counter3 = counter.clone();
        hooks.hook("multiHook", move || {
            let mut count = counter3.lock().unwrap();
            *count += 100;
        });

        // Execute all hooks
        hooks.call("multiHook").expect("Failed to call multiHook");

        // Verify all hooks were executed
        let final_count = *counter.lock().unwrap();
        assert_eq!(final_count, 111); // 1 + 10 + 100
    }

    #[test]
    fn test_sync_hook_called_with_async_method() {
        let hooks = base_hooks();
        let rt = Runtime::new().unwrap();

        let result = rt.block_on(hooks.call_async("finishLoad"));
        assert!(result.is_ok());
    }

    #[test]
    fn test_async_hook_called_with_sync_method() {
        let hooks = async_hooks();
        let result = hooks.call("asyncEvent");

        assert!(result.is_err());

        if let Err(e) = result {
            match e {
                HookableError::AsyncHookCalledSync(name) => {
                    assert_eq!(name, "asyncEvent");
                }
                _ => panic!("Expected AsyncHookCalledSync error"),
            }
        }
    }

    #[tokio::test]
    async fn test_basic_async_hook() {
        let hooks = async_hooks();
        let result = hooks.call_async("asyncEvent").await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_mixed_sync_and_async_hooks() {
        let hooks = Hookable::new();
        let counter = Arc::new(Mutex::new(0));

        let counter1 = counter.clone();
        hooks.hook("mixedEvent", move || {
            let mut count = counter1.lock().unwrap();
            *count += 1;
        });

        let counter2 = counter.clone();
        hooks.hook_async("mixedEvent", move || {
            let counter2 = counter2.clone();
            Box::pin(async move {
                sleep(Duration::from_millis(50)).await;
                let mut count = counter2.lock().unwrap();
                *count += 2;
            })
        });

        let counter3 = counter.clone();
        hooks.hook("mixedEvent", move || {
            let mut count = counter3.lock().unwrap();
            *count += 4;
        });

        hooks
            .call_async("mixedEvent")
            .await
            .expect("Failed to call mixedEvent");

        let final_count = *counter.lock().unwrap();
        assert_eq!(final_count, 7); // 1 + 2 + 4
    }

    #[tokio::test]
    async fn test_async_hook_not_found() {
        let hooks = Hookable::new();
        let result = hooks.call_async("nonExistentAsyncHook").await;
        assert!(result.is_err());

        if let Err(e) = result {
            match e {
                HookableError::NoHookFound(name) => {
                    assert_eq!(name, "nonExistentAsyncHook");
                }
                _ => panic!("Expected NoHookFound error"),
            }
        }
    }

    #[tokio::test]
    async fn test_multiple_async_hooks() {
        let hooks = Hookable::new();
        let results = Arc::new(Mutex::new(Vec::new()));

        let results1 = results.clone();
        hooks.hook_async("sequentialAsync", move || {
            let results1 = results1.clone();
            Box::pin(async move {
                sleep(Duration::from_millis(10)).await;
                results1.lock().unwrap().push("first");
            })
        });

        let results2 = results.clone();
        hooks.hook_async("sequentialAsync", move || {
            let results2 = results2.clone();
            Box::pin(async move {
                sleep(Duration::from_millis(20)).await;
                results2.lock().unwrap().push("second");
            })
        });

        let results3 = results.clone();
        hooks.hook_async("sequentialAsync", move || {
            let results3 = results3.clone();
            Box::pin(async move {
                sleep(Duration::from_millis(5)).await;
                results3.lock().unwrap().push("third");
            })
        });

        hooks
            .call_async("sequentialAsync")
            .await
            .expect("Failed to call sequentialAsync");

        let final_results = results.lock().unwrap();
        assert_eq!(final_results.len(), 3);
        assert_eq!(*final_results, vec!["first", "second", "third"]);
    }

    #[test]
    fn test_hookable_clone() {
        let hooks = Hookable::new();
        let counter = Arc::new(Mutex::new(0));

        let counter_clone = counter.clone();
        hooks.hook("cloneTest", move || {
            let mut count = counter_clone.lock().unwrap();
            *count += 1;
        });

        let hooks_clone = hooks.clone();

        hooks.call("cloneTest").expect("Failed to call on original");
        hooks_clone
            .call("cloneTest")
            .expect("Failed to call on clone");

        let final_count = *counter.lock().unwrap();
        assert_eq!(final_count, 2);
    }

    #[test]
    fn test_hookable_default() {
        let hooks = Hookable::default();

        hooks.hook("defaultTest", || {
            println!("Default hookable works");
        });

        let result = hooks.call("defaultTest");
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_concurrent_hook_execution() {
        let hooks = Arc::new(Hookable::new());
        let counter = Arc::new(Mutex::new(0));

        let counter_clone = counter.clone();
        hooks.hook("concurrentTest", move || {
            let mut count = counter_clone.lock().unwrap();
            *count += 1;
        });

        let mut handles = vec![];
        for _ in 0..10 {
            let hooks_clone = hooks.clone();
            let handle = tokio::spawn(async move {
                hooks_clone
                    .call("concurrentTest")
                    .expect("Failed to call hook");
            });
            handles.push(handle);
        }

        for handle in handles {
            handle.await.expect("Task failed");
        }

        let final_count = *counter.lock().unwrap();
        assert_eq!(final_count, 10);
    }

    #[test]
    fn test_hook_with_string_types() {
        let hooks = Hookable::new();

        hooks.hook("string_literal", || println!("String literal"));
        hooks.hook(String::from("owned_string"), || println!("Owned string"));
        hooks.hook("str_slice".to_string(), || println!("String slice"));

        assert!(hooks.call("string_literal").is_ok());
        assert!(hooks.call(String::from("owned_string")).is_ok());
        assert!(hooks.call("str_slice").is_ok());
    }
}