nonblocking-logger 0.3.0

A high-performance library with format string support
Documentation
use crate::enums::log_level::LogLevel;
use std::collections::HashMap;
use std::io;
use wasm_bindgen::prelude::*;

/// A simplified WASM logger that writes directly to browser console
/// No target management or message queuing needed
pub struct LoggerWasm {
    level: LogLevel,
    time_format: String,
    format_strings: HashMap<LogLevel, String>,
}

impl LoggerWasm {
    /// Create a new WASM logger with default settings
    pub fn new() -> Self {
        Self {
            level: LogLevel::Info,
            time_format: "%Y-%m-%d %H:%M:%S".to_string(),
            format_strings: Self::default_format_strings(),
        }
    }

    /// Create a WASM logger with a specific log level
    pub fn with_level(level: LogLevel) -> Self {
        Self {
            level,
            time_format: "%Y-%m-%d %H:%M:%S".to_string(),
            format_strings: Self::default_format_strings(),
        }
    }

    /// Set the time format using chrono format string
    pub fn time_format(mut self, format: &str) -> Self {
        self.time_format = format.to_string();
        self
    }

    /// Disable time prefix
    pub fn no_time_prefix(mut self) -> Self {
        self.time_format = String::new();
        self
    }

    /// Set the same custom format string for **all** log levels
    ///
    /// This overwrites the default per-level formats with a single format template.
    /// Placeholders:
    /// - `{time}`   - formatted timestamp (see `time_format`)
    /// - `{level}`  - log level (ERROR, WARN, INFO, DEBUG, TRACE)
    /// - `{message}` - the log message
    pub fn format(mut self, format: String) -> Self {
        let levels = [
            LogLevel::Error,
            LogLevel::Warning,
            LogLevel::Info,
            LogLevel::Debug,
            LogLevel::Trace,
        ];

        for level in levels {
            self.format_strings.insert(level, format.clone());
        }

        self
    }

    /// Set custom format string for a specific log level
    pub fn format_for_level(mut self, level: LogLevel, format: String) -> Self {
        self.format_strings.insert(level, format);
        self
    }

    /// Set the log level
    pub fn set_level(&mut self, level: LogLevel) {
        self.level = level;
    }

    /// Get the current log level
    pub fn get_level(&self) -> LogLevel {
        self.level
    }

    /// Get the current log level (alias for get_level for compatibility)
    pub fn level(&self) -> LogLevel {
        self.level
    }

    /// Set the time format
    pub fn set_time_format(&mut self, format: &str) {
        self.time_format = format.to_string();
    }

    /// Set format for a specific level
    pub fn set_format_for_level(&mut self, level: LogLevel, format: &str) {
        self.format_strings.insert(level, format.to_string());
    }

    /// Get default format strings for each log level
    fn default_format_strings() -> HashMap<LogLevel, String> {
        let mut formats = HashMap::new();
        formats.insert(LogLevel::Error, "{time} [ERROR] {message}".to_string());
        formats.insert(LogLevel::Warning, "{time} [WARN] {message}".to_string());
        formats.insert(LogLevel::Info, "{time} [INFO] {message}".to_string());
        formats.insert(LogLevel::Debug, "{time} [DEBUG] {message}".to_string());
        formats.insert(LogLevel::Trace, "{time} [TRACE] {message}".to_string());
        formats
    }

    /// Format a message according to the logger's settings
    fn format_message(&self, level: LogLevel, message: &str) -> String {
        let format_string = self
            .format_strings
            .get(&level)
            .cloned()
            .unwrap_or_else(|| Self::default_format_strings().get(&level).unwrap().clone());

        let time_str = if self.time_format.is_empty() {
            String::new()
        } else {
            use simple_datetime_rs::{DateTime, Format};
            DateTime::now()
                .format(&self.time_format)
                .unwrap_or_default()
        };

        format_string
            .replace("{time}", &time_str)
            .replace("{level}", &format!("{:?}", level))
            .replace("{message}", message)
    }

    /// Format a message without level information
    fn format_message_simple(&self, message: &str) -> String {
        let time_str = if self.time_format.is_empty() {
            String::new()
        } else {
            use simple_datetime_rs::{DateTime, Format};
            DateTime::now()
                .format(&self.time_format)
                .unwrap_or_default()
        };

        if time_str.is_empty() {
            message.to_string()
        } else {
            format!("{} {}", time_str, message)
        }
    }

    /// Log a message (always outputs, no level filtering)
    pub fn log(&self, message: &str) -> io::Result<()> {
        let formatted = self.format_message_simple(message);
        let js_message = JsValue::from_str(&formatted);

        let console = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("console"))
            .unwrap_or_else(|_| JsValue::UNDEFINED);

        if let Ok(method) = js_sys::Reflect::get(&console, &JsValue::from_str("log")) {
            if let Ok(func) = method.dyn_into::<js_sys::Function>() {
                let _ = func.call1(&console, &js_message);
            }
        }

        Ok(())
    }

    /// Log a message with lazy evaluation (always outputs, no level filtering)
    pub fn log_lazy<F>(&self, message_fn: F) -> io::Result<()>
    where
        F: FnOnce() -> String,
    {
        let message = message_fn();
        self.log(&message)
    }

    /// Log a message with lazy evaluation and specific level (with filtering)
    pub(crate) fn log_lazy_with_level<F>(&self, level: LogLevel, message_fn: F) -> io::Result<()>
    where
        F: FnOnce() -> String,
    {
        if level < self.level {
            return Ok(());
        }

        let message = message_fn();
        self.log_with_level(level, &message)
    }


    /// Log a message with a specific level (with filtering)
    pub(crate) fn log_with_level(&self, level: LogLevel, message: &str) -> io::Result<()> {
        if level < self.level {
            return Ok(());
        }

        let formatted = self.format_message(level, message);
        let js_message = JsValue::from_str(&formatted);

        let console = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("console"))
            .unwrap_or_else(|_| JsValue::UNDEFINED);

        let method_name = match level {
            LogLevel::Error => "error",
            LogLevel::Warning => "warn",
            LogLevel::Info => "info",
            LogLevel::Debug => "debug",
            LogLevel::Trace => "debug",
        };

        if let Ok(method) = js_sys::Reflect::get(&console, &JsValue::from_str(method_name)) {
            if let Ok(func) = method.dyn_into::<js_sys::Function>() {
                let _ = func.call1(&console, &js_message);
            }
        }

        Ok(())
    }

    /// Convenience methods for each log level
    pub fn error(&self, message: &str) -> io::Result<()> {
        self.log_with_level(LogLevel::Error, message)
    }

    pub fn warning(&self, message: &str) -> io::Result<()> {
        self.log_with_level(LogLevel::Warning, message)
    }

    pub fn info(&self, message: &str) -> io::Result<()> {
        self.log_with_level(LogLevel::Info, message)
    }

    pub fn debug(&self, message: &str) -> io::Result<()> {
        self.log_with_level(LogLevel::Debug, message)
    }

    pub fn trace(&self, message: &str) -> io::Result<()> {
        self.log_with_level(LogLevel::Trace, message)
    }

    /// Convenience methods for each log level with lazy evaluation
    pub fn error_lazy<F>(&self, message_fn: F) -> io::Result<()>
    where
        F: FnOnce() -> String,
    {
        self.log_lazy_with_level(LogLevel::Error, message_fn)
    }

    pub fn warning_lazy<F>(&self, message_fn: F) -> io::Result<()>
    where
        F: FnOnce() -> String,
    {
        self.log_lazy_with_level(LogLevel::Warning, message_fn)
    }

    pub fn info_lazy<F>(&self, message_fn: F) -> io::Result<()>
    where
        F: FnOnce() -> String,
    {
        self.log_lazy_with_level(LogLevel::Info, message_fn)
    }

    pub fn debug_lazy<F>(&self, message_fn: F) -> io::Result<()>
    where
        F: FnOnce() -> String,
    {
        self.log_lazy_with_level(LogLevel::Debug, message_fn)
    }

    pub fn trace_lazy<F>(&self, message_fn: F) -> io::Result<()>
    where
        F: FnOnce() -> String,
    {
        self.log_lazy_with_level(LogLevel::Trace, message_fn)
    }
}

impl Default for LoggerWasm {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(target_arch = "wasm32")]
#[cfg(test)]
mod wasm_tests {
    use wasm_bindgen_test::*;

    use super::*;

    #[wasm_bindgen_test]
    fn test_wasm_simple_logging() {
        let logger = LoggerWasm::new().no_time_prefix();

        assert_eq!(logger.level(), LogLevel::Info);

        logger
            .info("Hello, world!")
            .expect("Info logging should work");
        logger
            .warning("This is a warning")
            .expect("Warning logging should work");
        logger
            .error("This is an error")
            .expect("Error logging should work");

        assert_eq!(logger.level(), LogLevel::Info);
    }

    #[wasm_bindgen_test]
    fn test_wasm_time_format() {
        let logger = LoggerWasm::new().time_format("%Y-%m-%d %H:%M:%S");

        assert_eq!(logger.level(), LogLevel::Info);

        logger
            .info("Test message")
            .expect("Info logging with time format should work");
        logger
            .warning("Another test message")
            .expect("Warning logging should work");
    }

    #[wasm_bindgen_test]
    fn test_wasm_custom_format() {
        let logger = LoggerWasm::new()
            .no_time_prefix()
            .format_for_level(LogLevel::Error, "ERROR: {message}".to_string());

        assert_eq!(logger.level(), LogLevel::Info);

        logger
            .error("Test error message")
            .expect("Error logging with custom format should work");
        logger
            .info("This should use default format")
            .expect("Info logging should work");
    }

    #[wasm_bindgen_test]
    fn test_wasm_log_level_filtering() {
        let logger = LoggerWasm::with_level(LogLevel::Warning).no_time_prefix();

        assert_eq!(logger.level(), LogLevel::Warning);

        logger
            .warning("This should appear")
            .expect("Warning logging should work");
        logger
            .error("This should also appear")
            .expect("Error logging should work");

        logger
            .info("This should not appear")
            .expect("Info logging should return Ok even if filtered");
        logger
            .debug("This should not appear")
            .expect("Debug logging should return Ok even if filtered");

        assert_eq!(logger.level(), LogLevel::Warning);
    }

    #[wasm_bindgen_test]
    fn test_wasm_lazy_logging() {
        let logger = LoggerWasm::with_level(LogLevel::Info).no_time_prefix();

        let mut call_count = 0;
        logger
            .info_lazy(|| {
                call_count += 1;
                "Lazy message".to_string()
            })
            .expect("Lazy logging should work");

        assert_eq!(call_count, 1);
    }

    #[wasm_bindgen_test]
    fn test_wasm_global_logger_access() {
        let mut logger = LoggerWasm::new();

        assert_eq!(logger.level(), LogLevel::Info);

        logger.set_level(LogLevel::Debug);

        assert_eq!(logger.level(), LogLevel::Debug);

        logger
            .debug("Debug message after setting level")
            .expect("Debug logging should work after setting level");

        assert_eq!(logger.level(), LogLevel::Debug);
    }
}