leetcode-tui-core 0.5.2

Leetcode tui related core modules
Documentation
use indexmap::IndexSet;
use std::{collections::HashMap, hash::Hash, path::PathBuf, sync::RwLock};
use tokio::fs::read_to_string;

use leetcode_tui_config::CONFIG;
use leetcode_core::types::language::Language;
use regex::Regex;
use std::sync::OnceLock;

use crate::errors::{CoreError, CoreResult};
pub static FILENAME_REGEX: OnceLock<regex::Regex> = OnceLock::new();
pub static SOLUTION_FILE_MANAGER: OnceLock<RwLock<SolutionFileManager>> = OnceLock::new();

pub(crate) fn init() {
    SOLUTION_FILE_MANAGER
        .get_or_init(|| RwLock::new(CONFIG.as_ref().solutions_dir.clone().try_into().unwrap()));
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SolutionFile {
    path: PathBuf,
    pub(crate) question_id: String,
    pub(crate) title_slug: String,
    pub(crate) language: Language,
}

impl Hash for SolutionFile {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.question_id.hash(state);
        self.language.hash(state);
    }
}

impl SolutionFile {
    pub async fn read_contents(&self) -> CoreResult<String> {
        Ok(read_to_string(&self.path).await?)
    }
}

#[derive(Debug, Default)]
pub struct SolutionFileManager {
    id_language_map: HashMap<String, IndexSet<SolutionFile>>,
}

impl SolutionFileManager {
    fn add_solution_file(&mut self, file: SolutionFile) {
        self.id_language_map
            .entry(file.question_id.clone())
            .or_default()
            .insert(file);
    }

    pub(crate) fn create_solution_file(
        &mut self,
        file_name: &str,
        contents: &str,
    ) -> CoreResult<PathBuf> {
        let sol = &CONFIG.as_ref().solutions_dir;
        let file_path = sol.as_path().join(file_name);
        if !file_path.exists() {
            std::fs::write(file_path.as_path(), contents)?;
        }
        self.add_solution_file(file_path.clone().try_into()?);
        Ok(file_path.clone())
    }

    pub(crate) fn get_available_languages(&self, question_id: &str) -> CoreResult<Vec<&Language>> {
        self.id_language_map
            .get(question_id)
            .ok_or(CoreError::QuestionIdDoesNotExist(question_id.into()))
            .map(|v| v.iter().map(|sf| &sf.language).collect::<Vec<_>>())
    }

    pub(crate) fn get_solution_file(
        &self,
        question_id: &str,
        selected: usize,
    ) -> CoreResult<&SolutionFile> {
        self.id_language_map
            .get(question_id)
            .ok_or(CoreError::QuestionIdDoesNotExist(question_id.into()))
            .map(|v| v.get_index(selected).expect("Index does not exist"))
    }
}

impl TryFrom<PathBuf> for SolutionFileManager {
    type Error = CoreError;

    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
        let mut sfm = Self::default();
        for maybe_entry in std::fs::read_dir(value)? {
            let entry = maybe_entry?;
            let file_path = entry.path();
            let maybe_sol_file: CoreResult<SolutionFile> = file_path.try_into();
            if let Err(CoreError::FileNameDoesNotExistError(_)) = maybe_sol_file {
                continue;
            } else {
                let sol_file = maybe_sol_file?;
                sfm.add_solution_file(sol_file);
            }
        }
        Ok(sfm)
    }
}

impl TryFrom<PathBuf> for SolutionFile {
    type Error = CoreError;

    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
        let regex = FILENAME_REGEX.get_or_init(|| {
            Regex::new(r"0*(?P<q_id>\d*?)_(?P<slug>[\w-]*?)_(?P<lang_id>\d+).(?P<ext>\w+)")
                .expect("Could not compile regex.")
        });

        let err = Err(CoreError::FileNameFormatDoesNotMatch(value.clone()));

        let file_name = value
            .as_path()
            .file_name()
            .ok_or(CoreError::FileNameDoesNotExistError(value.clone()))?;

        let captures = regex.captures(
            file_name
                .to_str()
                .ok_or(CoreError::Utf8ValidityError(value.clone()))?,
        );

        let ids = ["q_id", "slug", "lang_id"];
        let mut res: [Option<&str>; 3] = [None, None, None];
        if let Some(_captures) = captures {
            for (i, cap_id) in ids.iter().enumerate() {
                res[i] = _captures.name(cap_id).map(|v| v.as_str());
            }
        } else {
            return err;
        }

        if let (Some(qid), Some(slug), Some(lang_id)) = (res[0], res[1], res[2]) {
            let lang: Language = lang_id.parse::<u32>()?.into();
            Ok(Self {
                question_id: qid.to_string(),
                title_slug: slug.to_string(),
                path: value,
                language: lang,
            })
        } else {
            err
        }
    }
}