use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::LazyLock;
use anyhow::Result;
use anyhow::bail;
use schemars::JsonSchema;
use crate::Config;
use crate::License;
#[derive(Clone, Debug, PartialEq, serde::Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Inventory {
pub authors: Vec<String>,
pub license: License,
pub documents: BTreeMap<String, Metadata>,
}
impl Inventory {
pub fn load(s: String, config: Arc<Config>) -> Result<Self> {
let inventory: UncheckedInventory = serde_json::from_str(&s)?;
Ok(Inventory {
authors: inventory.authors,
license: Self::validate_license(inventory.license_name, config.clone())?,
documents: inventory
.documents
.into_iter()
.map(|(filename, metadata)| -> Result<(String, Metadata)> {
Ok((
filename,
Metadata {
title: metadata.title,
authors: metadata.authors,
license: match metadata.license_name {
Some(name) => Some(Self::validate_license(name, config.clone())?),
None => None,
},
module: metadata.module,
},
))
})
.collect::<Result<_, _>>()?,
})
}
fn validate_license(name: String, config: Arc<Config>) -> Result<License> {
match config
.allowed_licenses
.clone()
.clone()
.into_iter()
.find(|license| license.name == name)
{
Some(license) => Ok(license),
None => bail!("unknown license `{}`", name),
}
}
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Metadata {
pub title: Option<String>,
#[serde(default)]
pub authors: Vec<String>,
pub license: Option<License>,
pub module: Module,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct UncheckedInventory {
pub authors: Vec<String>,
#[serde(rename = "license")]
pub license_name: String,
pub documents: BTreeMap<String, UncheckedMetadata>,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct UncheckedMetadata {
pub title: Option<String>,
#[serde(default)]
pub authors: Vec<String>,
#[serde(rename = "license")]
pub license_name: Option<String>,
pub module: Module,
}
#[derive(Clone, Debug, PartialEq, JsonSchema)]
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([
("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),
),
("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, JsonSchema)]
pub enum Semester1 {
Analysis1,
CircuitTheory,
ComputerTechnology,
DigitalTechnology,
LinearAlgebra,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize, JsonSchema)]
pub enum Semester2 {
AlgorithmsAndDataStructures,
Analysis2,
ElectrityAndMagnetism,
Physics,
SystemsTheory,
}
#[cfg(test)]
pub mod test {
use parameterized::parameterized;
use super::*;
use crate::config::test::example_config;
use crate::config::test::example_licenses;
#[test]
fn test_load_minimal_inventory() {
let (cc0, _) = example_licenses();
let config = example_config();
let expected_inventory = Inventory {
authors: vec![],
license: cc0,
documents: BTreeMap::new(),
};
let actual_inventory = Inventory::load(
r#"
{
"authors": [],
"license": "CC0",
"documents": {}
}
"#
.to_string(),
Arc::new(config),
)
.unwrap();
pretty_assertions::assert_eq!(expected_inventory, actual_inventory);
}
#[test]
fn test_load_example_inventory() {
let expected_inventory = example_inventory();
let config = example_config();
let actual_inventory = Inventory::load(raw_example_inventory(), Arc::new(config)).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 (cc0, _) = example_licenses();
let config = example_config();
let expected_inventory = Inventory {
authors: vec![],
license: cc0,
documents: BTreeMap::from([(
"document.pdf".to_string(),
Metadata {
title: None,
authors: vec![],
module: module,
license: None,
},
)]),
};
let actual_inventory = Inventory::load(
format!(
r#"
{{
"authors": [],
"license": "CC0",
"documents": {{
"document.pdf": {{
"module": "{}"
}}
}}
}}
"#,
raw_module
),
Arc::new(config),
)
.unwrap();
pretty_assertions::assert_eq!(expected_inventory, actual_inventory);
}
#[test]
#[should_panic]
fn test_load_inventory_with_unallowed_license() {
let config = example_config();
Inventory::load(
r#"
{
"authors": [],
"license": "My very custom license",
"documents": {}
}
"#
.to_string(),
Arc::new(config),
)
.unwrap();
}
#[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 {
let (cc0, cc_by_sa_4_0) = example_licenses();
Inventory {
authors: vec!["Alex".to_string()],
license: cc0,
documents: BTreeMap::from([
(
"sem1-analysis-1.pdf".to_string(),
Metadata {
title: None,
authors: vec![],
module: Module::Semester1(Semester1::Analysis1),
license: None,
},
),
(
"sem1-digitaltechnik.pdf".to_string(),
Metadata {
title: Some("DT".to_string()),
authors: vec![],
module: Module::Semester1(Semester1::DigitalTechnology),
license: None,
},
),
(
"sem1-schaltungstheorie.pdf".to_string(),
Metadata {
title: None,
authors: vec!["Alex".to_string(), "Max".to_string()],
module: Module::Semester1(Semester1::CircuitTheory),
license: Some(cc_by_sa_4_0.clone()),
},
),
(
"sem2-analysis-2.pdf".to_string(),
Metadata {
title: None,
authors: vec![],
module: Module::Semester2(Semester2::Analysis2),
license: None,
},
),
(
"sem2-systemtheorie.pdf".to_string(),
Metadata {
title: None,
authors: vec!["Max".to_string()],
module: Module::Semester2(Semester2::SystemsTheory),
license: Some(cc_by_sa_4_0),
},
),
]),
}
}
pub fn raw_example_inventory() -> String {
r#"
{
"authors": ["Alex"],
"license": "CC0",
"documents": {
"sem1-analysis-1.pdf": {
"module": "Analysis 1"
},
"sem1-digitaltechnik.pdf": {
"title": "DT",
"module": "Digitaltechnik"
},
"sem1-schaltungstheorie.pdf": {
"authors": ["Alex", "Max"],
"module": "Schaltungstheorie",
"license": "CC BY-SA 4.0"
},
"sem2-analysis-2.pdf": {
"module": "Analysis 2"
},
"sem2-systemtheorie.pdf": {
"authors": ["Max"],
"module": "Systemtheorie",
"license": "CC BY-SA 4.0"
}
}
}
"#
.to_string()
}
}