modcrawl 0.5.0

Detect, inspect, and analyze Minecraft mods and plugins from JAR files
Documentation
use std::fmt;

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::error::Result;

pub fn parse(input: &str) -> Result<ForgeLegacyMetadata> {
    Ok(serde_json::from_str(input)?)
}

pub type ForgeLegacyMetadata = Vec<ForgeLegacyModEntry>;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ForgeLegacyModEntry {
    pub modid: String,
    pub name: Option<String>,
    pub description: Option<String>,
    pub version: Option<String>,
    pub mcversion: Option<String>,
    pub url: Option<String>,
    #[serde(rename = "updateUrl")]
    pub update_url: Option<String>,
    #[serde(default)]
    #[serde(rename = "authorList")]
    pub author_list: Vec<String>,
    pub credits: Option<String>,
    #[serde(rename = "logoFile")]
    pub logo_file: Option<String>,
    #[serde(default)]
    pub screenshots: Vec<Value>,
    #[serde(default)]
    pub dependencies: Vec<String>,
    pub parent: Option<String>,
    #[serde(default)]
    pub authors: Vec<String>,
}

pub struct ForgeLegacyDisplay<'a>(pub &'a [ForgeLegacyModEntry]);

impl fmt::Display for ForgeLegacyModEntry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "Mod ID:   {}", self.modid)?;
        if let Some(n) = &self.name {
            writeln!(f, "Name:     {n}")?;
        }
        if let Some(d) = &self.description {
            writeln!(f, "About:    {d}")?;
        }
        if let Some(v) = &self.version {
            writeln!(f, "Version:  {v}")?;
        }
        if let Some(mv) = &self.mcversion {
            writeln!(f, "MC:       {mv}")?;
        }
        if !self.author_list.is_empty() {
            writeln!(f, "Authors:  {}", self.author_list.join(", "))?;
        }
        if !self.dependencies.is_empty() {
            writeln!(f, "Deps:     {}", self.dependencies.join(", "))?;
        }
        Ok(())
    }
}

impl fmt::Display for ForgeLegacyDisplay<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (i, e) in self.0.iter().enumerate() {
            if i > 0 {
                writeln!(f, "---")?;
            }
            write!(f, "{e}")?;
        }
        Ok(())
    }
}

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

    #[test]
    fn single_entry() {
        let json = r#"[{
            "modid": "testmod",
            "name": "Test Mod",
            "version": "1.0.0",
            "mcversion": "1.20.1",
            "description": "A test mod"
        }]"#;
        let meta = parse(json).unwrap();
        assert_eq!(meta.len(), 1);
        assert_eq!(meta[0].modid, "testmod");
        assert_eq!(meta[0].name.as_deref(), Some("Test Mod"));
        assert_eq!(meta[0].version.as_deref(), Some("1.0.0"));
    }

    #[test]
    fn multiple_entries() {
        let json = r#"[
            {"modid": "mod_a", "version": "1.0"},
            {"modid": "mod_b", "version": "2.0", "dependencies": ["mod_a"]}
        ]"#;
        let meta = parse(json).unwrap();
        assert_eq!(meta.len(), 2);
        assert_eq!(meta[1].dependencies, vec!["mod_a"]);
    }

    #[test]
    fn display_output() {
        let json = r#"[{
            "modid": "testmod",
            "name": "Test Mod",
            "version": "1.0.0"
        }]"#;
        let meta = parse(json).unwrap();
        let out = ForgeLegacyDisplay(&meta).to_string();
        assert!(out.contains("Mod ID:   testmod"));
        assert!(out.contains("Name:     Test Mod"));
        assert!(out.contains("Version:  1.0.0"));
    }
}