use crate::enums::log_level::LogLevel;
use std::collections::HashMap;
use std::io;
use wasm_bindgen::prelude::*;
pub struct LoggerWasm {
level: LogLevel,
time_format: String,
format_strings: HashMap<LogLevel, String>,
}
impl LoggerWasm {
pub fn new() -> Self {
Self {
level: LogLevel::Info,
time_format: "%Y-%m-%d %H:%M:%S".to_string(),
format_strings: Self::default_format_strings(),
}
}
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(),
}
}
pub fn time_format(mut self, format: &str) -> Self {
self.time_format = format.to_string();
self
}
pub fn no_time_prefix(mut self) -> Self {
self.time_format = String::new();
self
}
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
}
pub fn format_for_level(mut self, level: LogLevel, format: String) -> Self {
self.format_strings.insert(level, format);
self
}
pub fn set_level(&mut self, level: LogLevel) {
self.level = level;
}
pub fn get_level(&self) -> LogLevel {
self.level
}
pub fn level(&self) -> LogLevel {
self.level
}
pub fn set_time_format(&mut self, format: &str) {
self.time_format = format.to_string();
}
pub fn set_format_for_level(&mut self, level: LogLevel, format: &str) {
self.format_strings.insert(level, format.to_string());
}
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
}
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)
}
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)
}
}
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(())
}
pub fn log_lazy<F>(&self, message_fn: F) -> io::Result<()>
where
F: FnOnce() -> String,
{
let message = message_fn();
self.log(&message)
}
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)
}
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(())
}
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)
}
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);
}
}