handsome_logger 0.2.0

A fast, handsom and quite easy to use logger
Documentation
use log::LevelFilter;
use termcolor::{Color, ColorSpec};
pub use time::format_description::FormatItem;
pub use time::macros::format_description;
pub use time::UtcOffset;

const LEVEL_NUMBER: usize = 6;

#[derive(Debug, Clone, Copy)]
pub enum TimeFormat {
    Rfc2822,
    Rfc3339,
    Custom(&'static [FormatItem<'static>]),
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Token {
    Text(&'static str),
    Time,
    Level,
    ColorStart,
    ColorEnd,
    Thread,
    Module,
    FileName,
    File,
    Line,
    Message,
}

#[derive(Debug, Clone)]
pub struct Config {
    pub(crate) level: LevelFilter,
    pub(crate) time_offset: UtcOffset,

    pub(crate) time_format: [TimeFormat; LEVEL_NUMBER],
    pub(crate) format_text: [&'static str; LEVEL_NUMBER],
    pub(crate) tokens: [Vec<Token>; LEVEL_NUMBER],

    // Colors
    pub(crate) colored_text_color: [Option<Color>; LEVEL_NUMBER],
    pub(crate) background_color: [Option<Color>; LEVEL_NUMBER],
    pub(crate) compiled_colors: [ColorSpec; LEVEL_NUMBER],
    pub(crate) enabled_colors: bool,
}

const DEFAULT_FORMAT_TEXT: &str = "[_time] [_color_start][[_level]][_color_end] [_module]: [_msg]";

const CONF_FULL_FORMAT_TEST: &str = "[_time] [_color_start][[_level]][_color_end] [[_module]] [_file_name]:[_line] - [_msg]";

impl Config {
    /// Internal function to calculate all required data from user input
    /// this is done only once to avoid unnecessary computations
    pub(crate) fn calculate_data(&mut self) {
        self.calculate_tokens();
        self.calculate_colors();
    }

    /// Creating `ColorSpec` from user colors
    fn calculate_colors(&mut self) {
        for (idx, color_spec) in self.compiled_colors.iter_mut().enumerate() {
            *color_spec = ColorSpec::new().set_bg(self.background_color[idx]).set_fg(self.colored_text_color[idx]).clone();
        }
    }

    /// Calculate tokens from format text
    fn calculate_tokens(&mut self) {
        let allowed_tokens = [
            ("[_time]", Token::Time),
            ("[_level]", Token::Level),
            ("[_color_start]", Token::ColorStart),
            ("[_color_end]", Token::ColorEnd),
            ("[_thread]", Token::Thread),
            ("[_module]", Token::Module),
            ("[_file]", Token::File),
            ("[_file_name]", Token::FileName),
            ("[_line]", Token::Line),
            ("[_msg]", Token::Message),
        ];

        for (idx, format_text) in self.format_text.into_iter().enumerate() {
            let mut collected_tokens = Vec::new();
            let mut current_index = 0;
            loop {
                let mut minimum_index = usize::MAX;
                let mut choose_token_text = None;
                for (token_txt, token) in &allowed_tokens {
                    if let Some(find_idx) = format_text[current_index..].find(token_txt) {
                        if minimum_index > find_idx + current_index {
                            minimum_index = find_idx + current_index;
                            choose_token_text = Some((token_txt, token));
                        }
                    }
                }

                // No more tokens
                if let Some((token_txt, token)) = choose_token_text {
                    if minimum_index > current_index {
                        let text = &format_text[current_index..minimum_index];
                        collected_tokens.push(Token::Text(text));
                    }
                    collected_tokens.push(token.clone());
                    current_index = minimum_index + token_txt.len() - 1;
                } else {
                    if current_index != format_text.len() {
                        let text = &format_text[current_index..];
                        collected_tokens.push(Token::Text(text));
                    }
                    break; // No more to check
                }
                current_index += 1;
            }

            self.tokens[idx] = collected_tokens;
        }
    }
}

#[derive(Debug, Clone)]
pub struct ConfigBuilder(Config);

impl ConfigBuilder {
    #[must_use]
    pub fn new() -> ConfigBuilder {
        ConfigBuilder(Config::default())
    }

    /// Preset for saving bigger amount of information than default preset
    #[must_use]
    pub fn new_preset_config_full() -> ConfigBuilder {
        let mut builder = ConfigBuilder::default();
        builder.set_time_format(
            TimeFormat::Custom(format_description!(
                "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3] [offset_hour sign:mandatory]"
            )),
            None,
        );
        builder.set_format_text(CONF_FULL_FORMAT_TEST, None);
        builder
    }

    /// Sets format of logged message
    /// E.g. "[_time] [[_level]] [_module] \"[_msg]\""
    /// depending on other settings, may print something like:
    /// 14:21:15 [INFO] main: "Hello world!"
    /// If level is none, it will set all levels
    pub fn set_format_text(&mut self, format_text: &'static str, level: Option<LevelFilter>) -> &mut ConfigBuilder {
        if let Some(level) = level {
            self.0.format_text[level as usize] = format_text;
        } else {
            self.0.format_text = [format_text; LEVEL_NUMBER];
        }
        self
    }

    /// Sets background color
    /// If color is none, background will not be colored
    /// If level is none, it will set all levels
    /// If level is some, it will set only that level
    /// Background color is used only if `enabled_colors` is true
    pub fn set_background_color(&mut self, background_color: Option<Color>, level: Option<LevelFilter>) -> &mut ConfigBuilder {
        if let Some(level) = level {
            self.0.background_color[level as usize] = background_color;
        } else {
            self.0.background_color = [background_color; LEVEL_NUMBER];
        }
        self
    }

    /// Sets text color
    /// If color is none, text will be invisible
    /// If level is none, it will set all levels
    /// If level is some, it will set only that level
    /// Background color is used only if `enabled_colors` is true
    pub fn set_colored_text_color(&mut self, colored_text_color: Option<Color>, level: Option<LevelFilter>) -> &mut ConfigBuilder {
        if let Some(level) = level {
            self.0.colored_text_color[level as usize] = colored_text_color;
        } else {
            self.0.colored_text_color = [colored_text_color; LEVEL_NUMBER];
        }
        self
    }

    /// Enables colouring of text - only works with `TermLogger`
    pub fn set_enabled_colours(&mut self, enabled_colours: bool) -> &mut ConfigBuilder {
        self.0.enabled_colors = enabled_colours;
        self
    }

    /// Sets the level of the logger.
    /// E.g. using `LevelFilter::Info` will print all logs with level `Info`, `Warn`, `Error`,
    /// but not `Debug` or `Trace`.
    pub fn set_level(&mut self, level: LevelFilter) -> &mut ConfigBuilder {
        self.0.level = level;
        self
    }

    /// Set time format used in logger
    /// If level is none, it will set all levels
    /// Time format can be predefined(Rfc2822 or Rfc3339) or custom
    pub fn set_time_format(&mut self, time_format: TimeFormat, level: Option<LevelFilter>) -> &mut ConfigBuilder {
        if let Some(level) = level {
            self.0.time_format[level as usize] = time_format;
        } else {
            self.0.time_format = [time_format; LEVEL_NUMBER];
        }
        self
    }

    /// Manually sets the offset used for the time.
    pub fn set_time_offset(&mut self, offset: UtcOffset) -> &mut ConfigBuilder {
        self.0.time_offset = offset;
        self
    }

    /// Sets the offset used to the current local time offset
    /// (overriding values previously set by [`ConfigBuilder::set_time_offset`]).
    ///
    /// This function may fail if the offset cannot be determined soundly.
    /// This may be the case, when the program is multi-threaded by the time of calling this function.
    /// One can opt-out of this behavior by setting `RUSTFLAGS="--cfg unsound_local_offset"`.
    /// Doing so is not recommended, completely untested and may cause unexpected segfaults.
    #[cfg(feature = "time-local-offset")]
    pub fn set_time_offset_to_local(&mut self) -> Result<&mut ConfigBuilder, &mut ConfigBuilder> {
        match UtcOffset::current_local_offset() {
            Ok(offset) => {
                self.0.time_offset = offset;
                Ok(self)
            }
            Err(_) => Err(self),
        }
    }

    /// Sets the offset used to the current local time offset
    /// It is quite unsound so may cause crashes
    #[cfg(feature = "chrono-local-offset")]
    pub fn set_chrono_local_time_offset(&mut self) -> Result<&mut ConfigBuilder, &mut ConfigBuilder> {
        match UtcOffset::from_whole_seconds(chrono::offset::Local::now().offset().local_minus_utc()) {
            Ok(offset) => {
                self.0.time_offset = offset;
                Ok(self)
            }
            Err(_) => Err(self),
        }
    }

    /// Builds the config
    pub fn build(&mut self) -> Config {
        self.0.clone()
    }
}

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

impl Default for Config {
    fn default() -> Config {
        Config {
            level: LevelFilter::Info,
            time_format: [TimeFormat::Custom(format_description!("[hour]:[minute]:[second]")); LEVEL_NUMBER],
            time_offset: UtcOffset::UTC,

            tokens: [vec![], vec![], vec![], vec![], vec![], vec![]],
            colored_text_color: [
                None,
                Some(Color::Red),    // Error
                Some(Color::Yellow), // Warn
                Some(Color::Blue),   // Info
                Some(Color::Cyan),   // Debug
                Some(Color::White),  // Trace
            ],

            background_color: [None, None, None, None, None, None],
            enabled_colors: true,
            format_text: [DEFAULT_FORMAT_TEXT; LEVEL_NUMBER],
            compiled_colors: [ColorSpec::new(), ColorSpec::new(), ColorSpec::new(), ColorSpec::new(), ColorSpec::new(), ColorSpec::new()],
        }
    }
}

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

    #[test]
    fn test() {
        let text = "[_time] [_level] [_thread] [_module] [_file][_file_name] [_line] [_color_start][_msg][_color_end] [RAR]";
        let mut config = ConfigBuilder::new().set_format_text(text, None).build();
        config.calculate_data();
        assert_eq!(
            config.tokens[0],
            vec![
                Token::Time,
                Token::Text(" "),
                Token::Level,
                Token::Text(" "),
                Token::Thread,
                Token::Text(" "),
                Token::Module,
                Token::Text(" "),
                Token::File,
                Token::FileName,
                Token::Text(" "),
                Token::Line,
                Token::Text(" "),
                Token::ColorStart,
                Token::Message,
                Token::ColorEnd,
                Token::Text(" [RAR]"),
            ]
        );

        let text = "]]][[";
        let mut config = ConfigBuilder::new().set_format_text(text, None).build();
        config.calculate_data();
        assert_eq!(config.tokens[0], vec![Token::Text("]]][[")]);

        let text = " [_time]";
        let mut config = ConfigBuilder::new().set_format_text(text, None).build();
        config.calculate_data();
        assert_eq!(config.tokens[0], vec![Token::Text(" "), Token::Time]);

        let text = "[_time]";
        let mut config = ConfigBuilder::new().set_format_text(text, None).build();
        config.calculate_data();
        assert_eq!(config.tokens[0], vec![Token::Time]);

        let text = "[_time] ";
        let mut config = ConfigBuilder::new().set_format_text(text, None).build();
        config.calculate_data();
        assert_eq!(config.tokens[0], vec![Token::Time, Token::Text(" ")]);

        let text = "";
        let mut config = ConfigBuilder::new().set_format_text(text, None).build();
        config.calculate_data();
        assert_eq!(config.tokens[0], vec![]);
    }
}