quake_log_parser_lib 0.1.9

A Sample Lib to Parse Quake Game Log.
Documentation
use std::future::Future;
use std::pin::Pin;
use std::cell::RefCell;
use std::io::{self, BufRead};
use std::collections::{HashMap, HashSet};

use crate::interface::{ILogParser, LogParserCallBack, CallbackType, CallbackPayload};
use crate::errors::LogParserError;
use crate::config::static_config::{STATIC_CONFIG, StaticConfigParameter};
use crate::config::dynamic_config::{CONFIG, ConfigParameter};
use crate::death_causes::DeathCauses;
use crate::implementation::{
    death_causes::MatchKillMeans,
    match_data::MatchData,
    log_event::{
        LogEvent,
        KILL_PARSER_REGEX,
        USER_INFO_PARSER_REGEX
    },
};

thread_local!(pub static LOG_FILE_PATH: RefCell<Option<String>> = RefCell::new(None) );

pub(crate) struct ConcreteLogParser {
    success_callback: Option<Box<LogParserCallBack>>,
    warning_callback: Option<Box<LogParserCallBack>>,
    error_callback: Option<Box<LogParserCallBack>>,
    matches_data: Vec<MatchData>,
    current_match_data: MatchData,
    first_match: bool
}

impl ConcreteLogParser {
    pub(crate) fn new() -> Self {

        let mut show_death_causes: bool = false;

        CONFIG.with(|config| {
            show_death_causes = config.borrow().get_parameter(ConfigParameter::ShowDeathCauses).to_boolean();
        });

        Self {
            success_callback: None,
            warning_callback: None,
            error_callback: None,
            matches_data: Vec::<MatchData>::new(),
            current_match_data: MatchData {
                game_match: String::from(""),
                total_kills: 0,
                players: HashSet::new(),
                kills: HashMap::new(),
                kill_means: if show_death_causes { Some(MatchKillMeans::new()) } else { None } 
            },
            first_match: true
        }
    }

    async fn handle_callback(&self, cb_type: CallbackType, error: Option<LogParserError>, data: Option<String>) {
        
        match cb_type {
            CallbackType::Success => {
                if let Some(cb) = &self.success_callback {

                    let payload = CallbackPayload {
                        error: None,
                        data: data
                    }; 
        
                    if let Ok(pl) = serde_json::to_value(payload) {
                        let _res = cb(Some(pl)).await;
                    }
                }
            },
            CallbackType::Warning => {
                if let Some(cb) = &self.warning_callback {

                    let payload = CallbackPayload {
                        error: Some(error.unwrap().into()),
                        data: data
                    }; 
        
                    if let Ok(pl) = serde_json::to_value(payload) {
                        let _res = cb(Some(pl)).await;
                    }
                }
            },
            CallbackType::Error => {
                if let Some(cb) = &self.error_callback {

                    let payload = CallbackPayload {
                        error: Some(error.unwrap().into()),
                        data: data
                    }; 
        
                    if let Ok(pl) = serde_json::to_value(payload) {
                        let _res = cb(Some(pl)).await;
                    }
                }
            }
        }
        
    }

    fn get_match_label(&self) -> String {
        return format!("{}_{}", STATIC_CONFIG.get_parameter(StaticConfigParameter::OutputMatchKey).to_string().as_str(), self.matches_data.len());
    }
 
    fn register_new_match_stat(&mut self, match_stats: MatchData) {
        
        self.matches_data.push(
            match_stats
        );
    }

    async fn parse_log_line(&mut self, line: &str) -> Result<(), LogParserError> {

        match LogEvent::detect_line_log_event(line)? {

            LogEvent::InitMatch => {
                if self.first_match == false {

                    let mut show_death_causes: bool = false;

                    CONFIG.with(|config| {
                        show_death_causes = config.borrow().get_parameter(ConfigParameter::ShowDeathCauses).to_boolean();
                    });

                    self.current_match_data.game_match = self.get_match_label();

                    self.register_new_match_stat(
                        self.current_match_data.clone()
                    );
    
                    self.current_match_data = MatchData {
                        game_match: String::from(""),
                        total_kills: 0,
                        players: HashSet::new(),
                        kills: HashMap::new(),
                        kill_means: if show_death_causes { Some(MatchKillMeans::new()) } else { None }
                    };

                } else {
                    self.first_match = false;
                }

                return Ok(());
            },
            LogEvent::ClientConnect => {
                return Ok(());
            },
            LogEvent::ClientBegin => {
                return Ok(());
            },
            LogEvent::ClientUserinfoChanged => {
                if let Some(captures) = USER_INFO_PARSER_REGEX.captures(line) {
                    let player = &captures[1];

                    if !self.current_match_data.players.contains(player) {
                        self.current_match_data.players.insert(String::from(player));
                    }

                    return Ok(());
                } else {

                    self.handle_callback(
                        CallbackType::Warning,
                          Some(LogParserError::RegexParserError),
                           Some(String::from(line))
                     ).await;

                    return Err(LogParserError::RegexParserError);
                }
            },
            LogEvent::Item => {
                return Ok(());
            },
            LogEvent::Kill => {
                if let Some(captures) = KILL_PARSER_REGEX.captures(line) {

                    let mut show_death_causes: bool = false;
                    let mut self_kill_increases_score: bool = false;
                    let mut being_killed_decreases_score: bool = false;

                    CONFIG.with(|config| {
                        show_death_causes = config.borrow().get_parameter(ConfigParameter::ShowDeathCauses).to_boolean();
                        self_kill_increases_score = config.borrow().get_parameter(ConfigParameter::KillYourselfIncreasesScore).to_boolean();
                        being_killed_decreases_score = config.borrow().get_parameter(ConfigParameter::BeingKilledDecreasesScore).to_boolean();
                    });

                    let killer = &captures[3];
                    let player_killed = &captures[4];
                    let gun = &captures[5];
                    
                    self.current_match_data.total_kills += 1;

                    if killer == STATIC_CONFIG.get_parameter(StaticConfigParameter::WorldLogPattern).to_string().as_str() {
                        if let Some(kills) = self.current_match_data.kills.get(player_killed) {
                            self.current_match_data.kills.insert(String::from(player_killed), kills - 1);
                        } else {
                            self.current_match_data.kills.insert(String::from(player_killed), -1);
                        }
                    } else {

                        if killer != player_killed || self_kill_increases_score {
                            if let Some(kills) = self.current_match_data.kills.get(killer) {
                                self.current_match_data.kills.insert(String::from(killer), kills + 1);
                            } else {
                                self.current_match_data.kills.insert(String::from(killer), 1);
                            }
                        }

                        if being_killed_decreases_score {
                            if let Some(kills) = self.current_match_data.kills.get(player_killed) {
                                self.current_match_data.kills.insert(String::from(player_killed), kills - 1);
                            } else {
                                self.current_match_data.kills.insert(String::from(player_killed), -1);
                            }
                        }
                    }

                    if show_death_causes {
                        
                        if let Ok(death_cause) = DeathCauses::from_str(gun) {
                            self.current_match_data.kill_means.as_mut().unwrap().increase_stat(death_cause);
                        } else {
                            return Err(LogParserError::RegexParserError);
                        }
                    }

                    return Ok(());
                } else {
                    return Err(LogParserError::RegexParserError);
                }
            },
            LogEvent::ClientDisconnect => {
                return Ok(());
            },
            LogEvent::ShutdownGame => {
                return Ok(());
            },
            LogEvent::Exit => {
                return Ok(());
            },
        }
    }
}

impl ILogParser for ConcreteLogParser {
    
    fn register_success_callback(&mut self, callback: Box<LogParserCallBack>) {
        self.success_callback = Some(callback);
    }

    fn register_warning_callback(&mut self, callback: Box<LogParserCallBack>) {
        self.warning_callback = Some(callback);
    }

    fn register_error_callback(&mut self, callback: Box<LogParserCallBack>) {
        self.error_callback = Some(callback);
    }

    fn parse_file(&mut self) -> Pin<Box<dyn Future<Output = Result<String, LogParserError>> + '_>> {
        let future = async {

            let mut path:String = String::from(""); 

            CONFIG.with(|config| {
                if let Some(log_file_path) = config.borrow().get_parameter(ConfigParameter::LogFilePath).to_optional_string() {
                    path = log_file_path.clone();
                } else {
                    panic!("{}", STATIC_CONFIG.get_parameter(StaticConfigParameter::LogFilePathNotFoundErrMsg).to_string().as_str())
                }
            });

            let input = std::fs::File::open(path).map_err(|_e| LogParserError::ReadFileError)?;
            let reader = io::BufReader::new(input);
        
            for line in reader.lines() {
                
                let line = line.map_err(|_e| LogParserError::ReadFileError)?;
                
                if let Err(err) = self.parse_log_line(&line).await {
                    self.handle_callback(
                        CallbackType::Warning,
                        Some(err), 
                        Some(line)
                    ).await;
                }
            }

            let parsed_data = serde_json::to_value(&self.matches_data).map_err(|_e| LogParserError::SerializationError)?; 
            
            let stringfied_json = serde_json::to_string(&parsed_data).map_err(|_e| LogParserError::StringfyError)?;

            self.handle_callback(
                CallbackType::Success,
                None,
                Some(stringfied_json.clone())
             ).await;

            return Ok(stringfied_json);
        };

        return Box::pin(future);
    }
}