lux-lib 0.49.1

Library for the lux package manager for Lua
Documentation
use std::collections::VecDeque;
use std::sync::Mutex;

use lazy_static::lazy_static;
use serde::Serialize;

const MAX_BACKLOG: usize = 200;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
    Info,
    Warn,
    Error,
}

#[derive(Debug, Clone, Serialize)]
pub struct LogEntry {
    pub level: LogLevel,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoggingState {
    Disabled,
    Enabled,
}

lazy_static! {
    static ref LOG_BUFFER: Mutex<VecDeque<LogEntry>> = Mutex::new(VecDeque::new());
    static ref LOG_STATE: Mutex<LoggingState> = Mutex::new(LoggingState::Disabled);
}

#[allow(clippy::expect_used)]
pub fn set_state(state: LoggingState) {
    *LOG_STATE.lock().expect("log lock poisoned") = state;
}

#[allow(clippy::expect_used)]
pub fn state() -> LoggingState {
    *LOG_STATE.lock().expect("log lock poisoned")
}

#[allow(clippy::expect_used)]
pub fn info(message: String, target: Option<String>) {
    match state() {
        LoggingState::Enabled => push(LogLevel::Info, message, target),
        LoggingState::Disabled => println!("{}", message),
    }
}

pub fn warn(message: String, target: Option<String>) {
    match state() {
        LoggingState::Enabled => push(LogLevel::Warn, message, target),
        LoggingState::Disabled => eprintln!("{}", message),
    }
}

#[allow(clippy::expect_used)]
pub fn error(message: String, target: Option<String>) {
    match state() {
        LoggingState::Enabled => push(LogLevel::Error, message, target),
        LoggingState::Disabled => eprintln!("{}", message),
    }
}

#[allow(clippy::expect_used)]
pub fn push(level: LogLevel, message: String, target: Option<String>) {
    if let LoggingState::Disabled = state() {
        return;
    }

    let entry = LogEntry {
        level,
        message,
        target,
    };

    let mut buffer = LOG_BUFFER.lock().expect("log lock poisoned");

    buffer.push_back(entry);

    if buffer.len() > MAX_BACKLOG {
        buffer.pop_front();
    }
}

#[allow(clippy::expect_used)]
pub fn drain() -> Vec<LogEntry> {
    if let LoggingState::Disabled = state() {
        return Vec::new();
    }

    let mut buffer = LOG_BUFFER.lock().expect("log lock poisoned");
    buffer.drain(..).collect()
}

#[allow(clippy::expect_used)]
pub fn clear() {
    let mut buffer = LOG_BUFFER.lock().expect("log lock poisoned");

    buffer.clear();
}

#[cfg(test)]
mod tests {
    use serial_test::serial;

    use super::*;

    fn reset() {
        set_state(LoggingState::Disabled);
        clear();
    }

    #[test]
    #[serial]
    fn test_push_and_drain() {
        reset();
        set_state(LoggingState::Enabled);
        push(LogLevel::Info, "info".to_string(), None);
        push(
            LogLevel::Warn,
            "warning".to_string(),
            Some("build".to_string()),
        );
        push(
            LogLevel::Error,
            "error".to_string(),
            Some("fetch".to_string()),
        );

        let entries = drain();
        assert_eq!(entries.len(), 3);
        assert!(matches!(entries[0].level, LogLevel::Info));
        assert_eq!(entries[0].message, "info");
        assert!(entries[0].target.is_none());
        assert!(matches!(entries[1].level, LogLevel::Warn));
        assert_eq!(entries[1].message, "warning");
        assert_eq!(entries[1].target.as_deref(), Some("build"));
        assert!(matches!(entries[2].level, LogLevel::Error));

        assert!(drain().is_empty());
    }

    #[test]
    #[serial]
    fn test_push_noop_when_disabled() {
        reset();
        set_state(LoggingState::Disabled);
        push(LogLevel::Info, "should not appear".to_string(), None);
        assert!(drain().is_empty());
    }
}