ruzor 0.1.2

Ruzor, a 1:1-compatible Rust port of the Pyzor UDP client and server
Documentation
use std::fs::OpenOptions;
use std::io::{self, Write};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::local_time;

#[derive(Clone, Debug)]
pub struct Logger {
    file_path: Option<PathBuf>,
    debug: bool,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Level {
    Debug = 10,
    Info = 20,
    Warning = 30,
    Error = 40,
    Critical = 50,
}

impl Level {
    fn label(self) -> &'static str {
        match self {
            Self::Debug => "DEBUG",
            Self::Info => "INFO",
            Self::Warning => "WARNING",
            Self::Error => "ERROR",
            Self::Critical => "CRITICAL",
        }
    }
}

impl Logger {
    pub fn new(_name: &str, file_path: Option<String>, debug: bool) -> io::Result<Self> {
        let file_path = file_path.filter(|path| !path.is_empty()).map(PathBuf::from);
        if let Some(path) = &file_path {
            OpenOptions::new().create(true).append(true).open(path)?;
        }
        Ok(Self { file_path, debug })
    }

    pub fn debug(&self, message: impl AsRef<str>) {
        self.log(Level::Debug, message);
    }

    pub fn info(&self, message: impl AsRef<str>) {
        self.log(Level::Info, message);
    }

    pub fn warning(&self, message: impl AsRef<str>) {
        self.log(Level::Warning, message);
    }

    pub fn error(&self, message: impl AsRef<str>) {
        self.log(Level::Error, message);
    }

    pub fn critical(&self, message: impl AsRef<str>) {
        self.log(Level::Critical, message);
    }

    pub fn log(&self, level: Level, message: impl AsRef<str>) {
        let line = format!(
            "{} ({}) {} {}\n",
            timestamp(),
            std::process::id(),
            level.label(),
            message.as_ref()
        );

        if level >= self.stream_level() {
            eprint!("{line}");
        }
        if level >= self.file_level()
            && let Some(path) = &self.file_path
            && let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path)
        {
            let _ = file.write_all(line.as_bytes());
        }
    }

    fn stream_level(&self) -> Level {
        if self.debug {
            Level::Debug
        } else {
            Level::Critical
        }
    }

    fn file_level(&self) -> Level {
        if self.debug {
            Level::Debug
        } else {
            Level::Info
        }
    }
}

fn timestamp() -> String {
    let duration = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    format!(
        "{},{:03}",
        local_time::format_timestamp(duration.as_secs() as i64),
        duration.subsec_millis()
    )
}

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

    #[test]
    fn default_logger_writes_critical_to_stderr_level_and_info_to_file_level() {
        let logger = Logger {
            file_path: None,
            debug: false,
        };
        assert_eq!(logger.stream_level(), Level::Critical);
        assert_eq!(logger.file_level(), Level::Info);
    }

    #[test]
    fn debug_logger_writes_debug_to_stream_and_file() {
        let logger = Logger {
            file_path: None,
            debug: true,
        };
        assert_eq!(logger.stream_level(), Level::Debug);
        assert_eq!(logger.file_level(), Level::Debug);
    }
}