use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde_json::Value;
use crate::Linearization;
use crate::entity::{Entity, EntityError};
#[derive(Debug, thiserror::Error)]
pub enum CacheError {
#[error("cannot read {path}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("{path} is not JSON")]
Json {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("{path}: {source}")]
Entity {
path: PathBuf,
#[source]
source: EntityError,
},
#[error("no `{name}/<language>/_root.json` under {cache}")]
NoRoot {
cache: PathBuf,
name: &'static str,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cached {
pub linearization: Linearization,
pub release: Option<String>,
pub release_date: Option<String>,
pub titles: BTreeMap<String, String>,
pub languages: Vec<String>,
pub entities: BTreeMap<String, Entity>,
}
fn read_json(path: &Path) -> Result<Value, CacheError> {
let text = std::fs::read_to_string(path).map_err(|source| CacheError::Io {
path: path.to_path_buf(),
source,
})?;
serde_json::from_str(&text).map_err(|source| CacheError::Json {
path: path.to_path_buf(),
source,
})
}
pub fn read(cache: &Path, linearization: Linearization) -> Result<Cached, CacheError> {
let dir = cache.join(linearization.name());
let io = |path: &Path| {
let path = path.to_path_buf();
move |source| CacheError::Io { path, source }
};
let mut languages: Vec<String> = std::fs::read_dir(&dir)
.map_err(io(&dir))?
.filter_map(Result::ok)
.filter(|e| e.path().is_dir())
.filter_map(|e| e.file_name().to_str().map(str::to_owned))
.collect();
languages.sort();
let mut release = None;
let mut release_date = None;
let mut titles = BTreeMap::new();
let mut entities: BTreeMap<String, Entity> = BTreeMap::new();
for language in &languages {
let language_dir = dir.join(language);
let root_path = language_dir.join("_root.json");
if !root_path.exists() {
continue;
}
let root = read_json(&root_path)?;
if let Some(id) = root.get("releaseId").and_then(Value::as_str) {
release = Some(id.to_owned());
}
if let Some(date) = root.get("releaseDate").and_then(Value::as_str) {
release_date = Some(date.to_owned());
}
if let Some(title) = root
.get("title")
.and_then(|t| t.get("@value"))
.and_then(Value::as_str)
{
titles.insert(language.clone(), title.to_owned());
}
let mut files: Vec<PathBuf> = std::fs::read_dir(&language_dir)
.map_err(io(&language_dir))?
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| {
p.extension().is_some_and(|e| e == "json")
&& p.file_name().is_some_and(|n| n != "_root.json")
})
.collect();
files.sort();
for path in files {
let json = read_json(&path)?;
let entity =
Entity::parse(&json, linearization).map_err(|source| CacheError::Entity {
path: path.clone(),
source,
})?;
match entities.get_mut(&entity.id) {
Some(existing) => existing.merge_language(entity),
None => {
entities.insert(entity.id.clone(), entity);
}
}
}
}
if titles.is_empty() {
return Err(CacheError::NoRoot {
cache: cache.to_path_buf(),
name: linearization.name(),
});
}
Ok(Cached {
linearization,
release,
release_date,
titles,
languages,
entities,
})
}