#![allow(dead_code)]
use std::collections::HashMap;
use rust_embed::{EmbeddedFile, RustEmbed};
use crate::data::{Item, Monster};
#[derive(RustEmbed)]
#[folder = "$ASSETS_DIR/"]
pub struct Assets;
#[derive(Debug)]
pub struct Database {
pub monsters: Vec<Monster>,
pub items: Vec<Item>,
pub monster_index: HashMap<String, usize>,
pub item_index: HashMap<String, usize>,
}
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error("embedded asset not found: {0}")]
Missing(String),
#[error("failed to parse {file}: {error}")]
Parse { file: &'static str, error: String },
}
impl Database {
pub fn load() -> Result<Self, LoadError> {
let monsters: Vec<Monster> = parse_embedded("data/monsters.json")?;
let items: Vec<Item> = parse_embedded("data/items.json")?;
let monster_index = build_index(&monsters, |m| &m.slug);
let item_index = build_index(&items, |i| &i.slug);
Ok(Self {
monsters,
items,
monster_index,
item_index,
})
}
pub fn monster_by_slug(&self, slug: &str) -> Option<&Monster> {
self.monster_index.get(slug).map(|&i| &self.monsters[i])
}
pub fn item_by_slug(&self, slug: &str) -> Option<&Item> {
self.item_index.get(slug).map(|&i| &self.items[i])
}
pub fn asset(path: &str) -> Result<EmbeddedFile, LoadError> {
Assets::get(path).ok_or_else(|| LoadError::Missing(path.to_string()))
}
}
fn parse_embedded<T: serde::de::DeserializeOwned>(path: &'static str) -> Result<T, LoadError> {
let file = Database::asset(path)?;
serde_json::from_slice(&file.data).map_err(|e| LoadError::Parse {
file: path,
error: e.to_string(),
})
}
fn build_index<T, S: AsRef<str> + ?Sized>(
records: &[T],
slug: impl Fn(&T) -> &S,
) -> HashMap<String, usize> {
records
.iter()
.enumerate()
.fold(HashMap::with_capacity(records.len()), |mut acc, (i, r)| {
acc.entry(slug(r).as_ref().to_string()).or_insert(i);
acc
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loads_all_records() {
let db = Database::load().expect("embedded assets should parse");
assert!(
db.monsters.len() >= 300,
"monster count: {}",
db.monsters.len()
);
assert!(db.items.len() >= 1500, "item count: {}", db.items.len());
assert!(
db.monster_by_slug("rathalos").is_some(),
"rathalos should be indexed"
);
assert!(
db.item_by_slug("mega-potion").is_some(),
"mega-potion should be indexed"
);
}
#[test]
fn monster_index_dedupes_duplicate_slugs() {
let db = Database::load().expect("parse");
assert!(
db.monster_index.len() <= db.monsters.len(),
"index ({}) should never exceed records ({})",
db.monster_index.len(),
db.monsters.len()
);
assert!(db.monster_by_slug("rathalos").is_some());
assert!(db.monster_by_slug("versa-pietru").is_some());
}
#[test]
fn every_monster_has_ja_and_zh() {
let db = Database::load().expect("parse");
let offenders: Vec<&str> = db
.monsters
.iter()
.filter(|m| m.i18n.ja.is_none() || m.i18n.zh.is_none())
.map(|m| m.slug.as_str())
.collect();
assert!(offenders.is_empty(), "monsters missing i18n: {offenders:?}");
}
#[test]
fn rathalos_has_all_three_numeric_subkeys() {
let db = Database::load().expect("parse");
let m = db.monster_by_slug("rathalos").expect("rathalos");
let n = m.numeric.as_ref().expect("rathalos has numeric");
assert!(n.mhw.is_some(), "missing mhw");
assert!(n.wilds.is_some(), "missing wilds");
assert!(n.mhgu.is_some(), "missing mhgu");
}
}