typst4ei 2.0.0-alpha

The Typst4EI building and serving tool/cli
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::LazyLock;

use anyhow::Result;

#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Inventory {
    pub authors: Vec<String>,
    pub documents: BTreeMap<String, Metadata>,
}

#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Metadata {
    pub title: Option<String>,
    #[serde(default)]
    pub authors: Vec<String>,
    pub module: Module,
}

impl Inventory {
    pub fn load(s: String) -> Result<Self> {
        Ok(serde_json::from_str(&s)?)
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum Module {
    Semester1(Semester1),
    Semester2(Semester2),
    Misc,
}

impl Module {
    pub fn category(&self) -> String {
        String::from(match self {
            Module::Semester1(_) => "Semester 1",
            Module::Semester2(_) => "Semester 2",
            Module::Misc => "Verschiedenes",
        })
    }

    pub fn name(&self) -> String {
        for (name, module) in MODULES.clone() {
            if *self == module {
                return name.to_string();
            }
        }
        unreachable!();
    }
}

impl<'de> serde::Deserialize<'de> for Module {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let name = <&str>::deserialize(deserializer)?;
        match MODULES.get(name) {
            Some(module) => Ok(module.clone()),
            None => Err(serde::de::Error::custom(format!(
                "unknown module `{}`",
                name
            ))),
        }
    }
}

static MODULES: LazyLock<HashMap<&str, Module>> = LazyLock::new(|| {
    HashMap::from([
        // Semester 1
        ("Analysis 1", Module::Semester1(Semester1::Analysis1)),
        (
            "Schaltungstheorie",
            Module::Semester1(Semester1::CircuitTheory),
        ),
        (
            "Digitaltechnik",
            Module::Semester1(Semester1::DigitalTechnology),
        ),
        (
            "Computertechnik",
            Module::Semester1(Semester1::ComputerTechnology),
        ),
        (
            "Lineare Algebra",
            Module::Semester1(Semester1::LinearAlgebra),
        ),
        // Semester 2
        ("Analysis 2", Module::Semester2(Semester2::Analysis2)),
        ("Systemtheorie", Module::Semester2(Semester2::SystemsTheory)),
        (
            "Elektrizität und Magnetismus",
            Module::Semester2(Semester2::ElectrityAndMagnetism),
        ),
        ("Physik", Module::Semester2(Semester2::Physics)),
        (
            "Algorithmen und Datenstrukturen",
            Module::Semester2(Semester2::AlgorithmsAndDataStructures),
        ),
        //
        ("Verschiedenes", Module::Misc),
    ])
});

#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
pub enum Semester1 {
    Analysis1,
    CircuitTheory,
    ComputerTechnology,
    DigitalTechnology,
    LinearAlgebra,
}

#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
pub enum Semester2 {
    AlgorithmsAndDataStructures,
    Analysis2,
    ElectrityAndMagnetism,
    Physics,
    SystemsTheory,
}

#[cfg(test)]
mod test {
    use parameterized::parameterized;

    use super::*;

    #[test]
    fn test_load_minimal_inventory() {
        let expected_inventory = Inventory {
            authors: vec![],
            documents: BTreeMap::new(),
        };
        let actual_inventory = Inventory::load(
            r#"
{
    "authors": [],
    "documents": {}
}
"#
            .to_string(),
        )
        .unwrap();

        pretty_assertions::assert_eq!(expected_inventory, actual_inventory);
    }

    #[test]
    fn test_load_example_inventory() {
        let expected_inventory = example_inventory();
        let actual_inventory = Inventory::load(raw_example_inventory()).unwrap();

        pretty_assertions::assert_eq!(expected_inventory, actual_inventory);
    }

    #[parameterized(raw_module = {
	"Analysis 1",
	"Computertechnik",
	"Digitaltechnik",
	"Lineare Algebra",
	"Schaltungstheorie",
	"Algorithmen und Datenstrukturen",
	"Analysis 2",
	"Elektrizität und Magnetismus",
	"Physik",
	"Systemtheorie",
	"Verschiedenes",
    }, module = {
	Module::Semester1(Semester1::Analysis1),
	Module::Semester1(Semester1::ComputerTechnology),
	Module::Semester1(Semester1::DigitalTechnology),
	Module::Semester1(Semester1::LinearAlgebra),
	Module::Semester1(Semester1::CircuitTheory),
	Module::Semester2(Semester2::AlgorithmsAndDataStructures),
	Module::Semester2(Semester2::Analysis2),
	Module::Semester2(Semester2::ElectrityAndMagnetism),
	Module::Semester2(Semester2::Physics),
	Module::Semester2(Semester2::SystemsTheory),
	Module::Misc,
    })]
    fn test_load_inventory_with_different_modules(raw_module: &str, module: Module) {
        let expected_inventory = Inventory {
            authors: vec![],
            documents: BTreeMap::from([(
                "document.pdf".to_string(),
                Metadata {
                    title: None,
                    authors: vec![],
                    module: module,
                },
            )]),
        };
        let actual_inventory = Inventory::load(format!(
            r#"
{{
    "authors": [],
    "documents": {{
        "document.pdf": {{
            "module": "{}"
        }}
    }}
}}
"#,
            raw_module
        ))
        .unwrap();

        pretty_assertions::assert_eq!(expected_inventory, actual_inventory);
    }

    #[parameterized(module = {
	Module::Semester1(Semester1::Analysis1),
	Module::Semester1(Semester1::ComputerTechnology),
	Module::Semester1(Semester1::DigitalTechnology),
	Module::Semester1(Semester1::LinearAlgebra),
	Module::Semester1(Semester1::CircuitTheory),
	Module::Semester2(Semester2::AlgorithmsAndDataStructures),
	Module::Semester2(Semester2::Analysis2),
	Module::Semester2(Semester2::ElectrityAndMagnetism),
	Module::Semester2(Semester2::Physics),
	Module::Semester2(Semester2::SystemsTheory),
	Module::Misc,
    }, expected_category = {
	"Semester 1",
	"Semester 1",
    	"Semester 1",
    	"Semester 1",
    	"Semester 1",
    	"Semester 2",
	"Semester 2",
	"Semester 2",
	"Semester 2",
	"Semester 2",
	"Verschiedenes",
    })]
    fn test_module_category(module: Module, expected_category: &str) {
        assert_eq!(expected_category, module.category());
    }

    #[parameterized(module = {
	Module::Semester1(Semester1::Analysis1),
	Module::Semester1(Semester1::ComputerTechnology),
	Module::Semester1(Semester1::DigitalTechnology),
	Module::Semester1(Semester1::LinearAlgebra),
	Module::Semester1(Semester1::CircuitTheory),
	Module::Semester2(Semester2::AlgorithmsAndDataStructures),
	Module::Semester2(Semester2::Analysis2),
	Module::Semester2(Semester2::ElectrityAndMagnetism),
	Module::Semester2(Semester2::Physics),
	Module::Semester2(Semester2::SystemsTheory),
	Module::Misc,
    }, expected_name = {
	"Analysis 1",
	"Computertechnik",
	"Digitaltechnik",
	"Lineare Algebra",
	"Schaltungstheorie",
	"Algorithmen und Datenstrukturen",
	"Analysis 2",
	"Elektrizität und Magnetismus",
	"Physik",
	"Systemtheorie",
	"Verschiedenes",
    })]
    fn test_module_name(module: Module, expected_name: &str) {
        assert_eq!(expected_name, module.name());
    }

    fn example_inventory() -> Inventory {
        Inventory {
            authors: vec!["Alex".to_string()],
            documents: BTreeMap::from([
                (
                    "sem1-analysis-1.pdf".to_string(),
                    Metadata {
                        title: None,
                        authors: vec![],
                        module: Module::Semester1(Semester1::Analysis1),
                    },
                ),
                (
                    "sem1-digitaltechnik.pdf".to_string(),
                    Metadata {
                        title: Some("DT".to_string()),
                        authors: vec![],
                        module: Module::Semester1(Semester1::DigitalTechnology),
                    },
                ),
                (
                    "sem1-schaltungstheorie.pdf".to_string(),
                    Metadata {
                        title: None,
                        authors: vec!["Alex".to_string(), "Max".to_string()],
                        module: Module::Semester1(Semester1::CircuitTheory),
                    },
                ),
                (
                    "sem2-analysis-2.pdf".to_string(),
                    Metadata {
                        title: None,
                        authors: vec![],
                        module: Module::Semester2(Semester2::Analysis2),
                    },
                ),
                (
                    "sem2-systemtheorie.pdf".to_string(),
                    Metadata {
                        title: None,
                        authors: vec!["Max".to_string()],
                        module: Module::Semester2(Semester2::SystemsTheory),
                    },
                ),
            ]),
        }
    }

    fn raw_example_inventory() -> String {
        r#"
{
    "authors": ["Alex"],
    "documents": {
        "sem1-analysis-1.pdf": {
            "module": "Analysis 1"
        },
        "sem1-digitaltechnik.pdf": {
            "title": "DT",
            "module": "Digitaltechnik"
        },
        "sem1-schaltungstheorie.pdf": {
            "authors": ["Alex", "Max"],
            "module": "Schaltungstheorie"
        },
        "sem2-analysis-2.pdf": {
            "module": "Analysis 2"
        },
        "sem2-systemtheorie.pdf": {
            "authors": ["Max"],
            "module": "Systemtheorie"
        }
    }
}
"#
        .to_string()
    }
}