sculkr 0.2.0

Companion CLI for packwiz - generate modlists and track Minecraft modpack changes
use std::{fs::OpenOptions, io::Read, path::PathBuf};

use serde::{Deserialize, Serialize};

use crate::{
    Error,
    error::IoContext,
    parser::{ParsedCurseForgeId, ParsedModrinthId, Parser},
};

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PackwizModUpdate {
    pub modrinth: Option<PackwizModUpdateModrinth>,
    pub curseforge: Option<PackwizModUpdateCurseforge>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PackwizModUpdateModrinth {
    pub mod_id: String,
    pub version: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PackwizModUpdateCurseforge {
    pub file_id: u32,
    pub project_id: i32,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PackwizMod {
    pub name: String,
    pub filename: String,
    pub update: PackwizModUpdate,
}

#[derive(Debug, Clone)]
pub struct PackwizParser {
    pub modrinth_mods: Vec<ParsedModrinthId>,
    pub curseforge_mods: Vec<ParsedCurseForgeId>,
}

impl PackwizParser {
    pub fn load_from<T>(directory: T) -> Result<Self, Error>
    where
        T: Into<PathBuf>,
    {
        let directory = directory.into();

        let resolved = crate::util::resolve_for_display(&directory);

        log::debug!("scanning for *.pw.toml in \"{}\"", resolved.display());

        let entries = directory.read_dir().path_ctx(&resolved, "read directory")?;
        let mut parsed_mods = Vec::new();
        let mut skipped = 0usize;

        for entry in entries {
            let entry = entry.path_ctx(&resolved, "read directory entry")?;
            let path = entry.path();

            if !entry.file_name().to_string_lossy().ends_with(".pw.toml") {
                skipped += 1;
                log::trace!("skipping non-pw.toml entry \"{}\"", path.display());
                continue;
            }

            let data = std::fs::read_to_string(&path).path_ctx(&path, "read file")?;
            let parsed = toml::from_str::<PackwizMod>(&data)
                .map_err(|err| Error::TomlFile(path.clone(), err))?;

            log::debug!("parsed \"{}\" as \"{}\"", path.display(), parsed.name);
            parsed_mods.push(parsed);
        }

        log::info!(
            "found {} mod(s) in \"{}\" ({} entries skipped)",
            parsed_mods.len(),
            resolved.display(),
            skipped
        );

        if parsed_mods.is_empty() {
            log::warn!("no *.pw.toml files found in \"{}\"", resolved.display());
        }

        let modrinth_mods = parsed_mods
            .clone()
            .into_iter()
            .filter_map(|m| m.update.modrinth)
            .map(|data| ParsedModrinthId {
                cache_id: data.version,
                id: data.mod_id,
            })
            .collect();

        let curseforge_mods = parsed_mods
            .into_iter()
            .filter_map(|m| m.update.curseforge)
            .map(|data| ParsedCurseForgeId {
                cache_id: data.file_id.to_string(),
                id: data.project_id,
            })
            .collect();

        Ok(Self {
            modrinth_mods,
            curseforge_mods,
        })
    }
}

impl Parser for PackwizParser {
    fn get_mods_owned(self) -> (Vec<ParsedModrinthId>, Vec<ParsedCurseForgeId>) {
        (self.modrinth_mods, self.curseforge_mods)
    }

    fn get_modrinth_mods(&self) -> Vec<ParsedModrinthId> {
        self.modrinth_mods.clone()
    }

    fn get_curseforge_mods(&self) -> Vec<ParsedCurseForgeId> {
        self.curseforge_mods.clone()
    }
}