kelora 1.5.0

A command-line log analysis tool with embedded Rhai scripting
Documentation
use rhai::{Dynamic, Engine, ImmutableString};
use std::cell::Cell;

// Use thread-local storage for exit state to handle parallel processing correctly
thread_local! {
    static EXIT_REQUESTED: Cell<bool> = const { Cell::new(false) };
    static EXIT_CODE: Cell<i32> = const { Cell::new(0) };
    static SKIP_REQUESTED: Cell<bool> = const { Cell::new(false) };
}

/// Rhai function: exit(code: INT, msg: STRING = null)
/// Immediately stops all event processing and terminates Kelora with the given exit code.
pub fn exit_process(code: i64, msg: Dynamic) -> Dynamic {
    // Store exit code (clamp to valid range for process exit codes)
    let exit_code = code.clamp(0, 255) as i32;
    EXIT_CODE.with(|ec| ec.set(exit_code));

    // Print message to stderr if provided
    if !msg.is_unit() {
        if let Some(s) = msg.read_lock::<ImmutableString>() {
            eprintln!("{}", s.as_str());
        } else {
            eprintln!("{}", msg);
        }
    }

    // Set exit flag
    EXIT_REQUESTED.with(|er| er.set(true));

    // In testing, don't actually exit - just set the flags
    #[cfg(not(test))]
    {
        std::process::exit(exit_code);
    }

    // Return unit to indicate function completed (only reached in tests)
    #[cfg(test)]
    Dynamic::UNIT
}

/// Check if exit has been requested from Rhai scripts
pub fn is_exit_requested() -> bool {
    EXIT_REQUESTED.with(|er| er.get())
}

/// Get the requested exit code
pub fn get_exit_code() -> i32 {
    EXIT_CODE.with(|ec| ec.get())
}

/// Rhai function: skip() - mark the current event to be skipped (filtered) and continue processing the next event.
pub fn skip_event() -> Dynamic {
    SKIP_REQUESTED.with(|skip| skip.set(true));
    Dynamic::UNIT
}

/// Check if skip has been requested for the current event and clear the flag.
pub fn take_skip_request() -> bool {
    SKIP_REQUESTED.with(|skip| {
        let requested = skip.get();
        skip.set(false);
        requested
    })
}

/// Check without clearing if a skip was requested (used for testing/verification).
#[cfg(test)]
pub fn is_skip_requested() -> bool {
    SKIP_REQUESTED.with(|skip| skip.get())
}

/// Clear any pending skip request (used to reset state between events/tests).
pub fn clear_skip_request() {
    SKIP_REQUESTED.with(|skip| skip.set(false));
}

/// Reset exit state (useful for testing)
#[cfg(test)]
pub fn reset_exit_state() {
    EXIT_REQUESTED.with(|er| er.set(false));
    EXIT_CODE.with(|ec| ec.set(0));
    SKIP_REQUESTED.with(|skip| skip.set(false));
}

/// Rhai function wrapper for single parameter: exit(code)
pub fn exit_process_single(code: i64) -> Dynamic {
    exit_process(code, Dynamic::UNIT)
}

/// Register process control functions with the Rhai engine
pub fn register_functions(engine: &mut Engine) {
    engine.register_fn("exit", exit_process_single);
    engine.register_fn("exit", exit_process);
    engine.register_fn("skip", skip_event);
}

#[cfg(test)]
mod tests {
    use super::*;
    use rhai::Engine;

    #[test]
    fn test_exit_with_code_only() {
        reset_exit_state();

        let result = exit_process(42, Dynamic::UNIT);

        assert!(result.is_unit());
        assert!(is_exit_requested());
        assert_eq!(get_exit_code(), 42);
    }

    #[test]
    fn test_exit_with_message() {
        reset_exit_state();

        let msg = Dynamic::from("Test error message");
        let result = exit_process(1, msg);

        assert!(result.is_unit());
        assert!(is_exit_requested());
        assert_eq!(get_exit_code(), 1);
    }

    #[test]
    fn test_exit_code_clamping() {
        reset_exit_state();

        // Test negative code
        let _ = exit_process(-5, Dynamic::UNIT);
        assert_eq!(get_exit_code(), 0);

        reset_exit_state();

        // Test code > 255
        let _ = exit_process(300, Dynamic::UNIT);
        assert_eq!(get_exit_code(), 255);
    }

    #[test]
    fn test_rhai_integration() {
        reset_exit_state();

        let mut engine = Engine::new();
        register_functions(&mut engine);

        // Test exit with code only
        let result = engine.eval::<Dynamic>("exit(123)");
        if let Err(e) = &result {
            eprintln!("Rhai error: {}", e);
        }
        assert!(result.is_ok());

        eprintln!("Exit requested: {}", is_exit_requested());
        eprintln!("Exit code: {}", get_exit_code());

        assert!(is_exit_requested());
        assert_eq!(get_exit_code(), 123);

        reset_exit_state();

        // Test exit with message
        let result = engine.eval::<Dynamic>(r#"exit(1, "Error occurred")"#);
        assert!(result.is_ok());
        assert!(is_exit_requested());
        assert_eq!(get_exit_code(), 1);
    }

    #[test]
    fn test_skip_request_lifecycle() {
        reset_exit_state();
        assert!(!is_skip_requested());

        let result = skip_event();
        assert!(result.is_unit());
        assert!(is_skip_requested());
        assert!(take_skip_request());
        assert!(!is_skip_requested());
    }
}