use std::collections::BTreeMap;
use std::path::Path;
use crate::composition::Compositions;
use crate::kits::{self, Bound, Kits};
use crate::map::Map;
use crate::rules::{self, Rules};
use crate::vocab::Vocabularies;
pub const VOCAB_FILE: &str = "vocab.ron";
pub const MAPS_DIR: &str = "maps";
pub fn migrate_legacy_data_dir(root: &Path) -> Result<Option<String>, String> {
let old = root.join("assets/emerge");
let new = root.join("assets/grimoire");
match (old.is_dir(), new.exists()) {
(true, true) => Err(format!(
"both assets/emerge and assets/grimoire exist in {}; keep one: Grimoire reads assets/grimoire",
root.display()
)),
(true, false) => {
std::fs::rename(&old, &new)
.map_err(|e| format!("cannot move {} to {}: {e}", old.display(), new.display()))?;
Ok(Some("moved assets/emerge to assets/grimoire (the editor was renamed to Grimoire)".into()))
}
(false, _) => Ok(None),
}
}
#[derive(Clone, Debug)]
pub struct ProjectData {
pub kits: Kits,
pub bound: Bound,
pub vocab: Vocabularies,
pub rules: Rules,
pub compositions: Compositions,
pub maps: BTreeMap<String, Map>,
}
impl ProjectData {
pub fn load(grimoire_dir: &Path, maps: &[&str]) -> Result<ProjectData, String> {
load(grimoire_dir, maps)
}
}
fn read(path: &Path) -> Result<String, String> {
std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))
}
pub fn load(grimoire_dir: &Path, maps: &[&str]) -> Result<ProjectData, String> {
let kits_path = grimoire_dir.join(kits::KITS_FILE);
let kits = Kits::parse(&read(&kits_path)?).map_err(|e| format!("{}: {e}", kits_path.display()))?;
let bound = kits::bound_library(grimoire_dir, &kits)?;
let vocab_path = grimoire_dir.join(VOCAB_FILE);
let vocab =
Vocabularies::parse(&read(&vocab_path)?).map_err(|e| format!("{}: {e}", vocab_path.display()))?;
bound
.library
.validate_lattices(kits.lattice.face_bands)
.map_err(|e| format!("{}: {e}", grimoire_dir.display()))?;
bound
.library
.resolve(&vocab)
.map_err(|e| format!("{}: {e}", grimoire_dir.display()))?;
let compositions_path = grimoire_dir.join(Compositions::FILE);
vocab
.check_slots(&bound.compositions.compositions)
.map_err(|e| format!("{}: {e}", compositions_path.display()))?;
let rules_path = grimoire_dir.join(rules::FILE);
let rules = if rules_path.is_file() {
let parsed =
Rules::parse(&read(&rules_path)?).map_err(|e| format!("{}: {e}", rules_path.display()))?;
parsed
.validate_against(&bound.library)
.map_err(|e| format!("{}: {e}", rules_path.display()))?;
parsed
} else {
Rules {
version: rules::RULES_VERSION,
blocks: Vec::new(),
}
};
let ids: Vec<String> = bound.library.descriptors.iter().map(|d| d.id.clone()).collect();
let mut loaded = BTreeMap::new();
for name in maps {
let path = grimoire_dir
.join(MAPS_DIR)
.join(crate::naming::map_file_name(name));
let map = Map::parse(&read(&path)?).map_err(|e| format!("{}: {e}", path.display()))?;
if map.name != *name {
return Err(format!(
"{}: calls itself `{}`. A map's name IS its file.",
path.display(),
map.name
));
}
let map = map
.expanded(&bound.compositions.compositions, &bound.library)
.map_err(|e| format!("{}: {e}", path.display()))?;
map.validate_against(&ids)
.map_err(|e| format!("{}: {e}", path.display()))?;
loaded.insert((*name).to_owned(), map);
}
let compositions = bound.compositions.clone();
Ok(ProjectData {
kits,
bound,
vocab,
rules,
compositions,
maps: loaded,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/grimoire")
}
#[test]
fn the_fixture_project_loads_whole() {
let data = load(&fixture(), &[]).unwrap_or_else(|e| panic!("{e}"));
assert!(!data.kits.bind.is_empty(), "the fixture binds kits");
assert_eq!(data.bound.kits.len(), data.kits.bind.len());
assert!(!data.bound.library.descriptors.is_empty(), "the merged library has pieces");
assert!(data.rules.blocks.is_empty(), "no rules.ron means no block rules");
assert_eq!(data.rules.version, rules::RULES_VERSION);
assert_eq!(data.compositions, data.bound.compositions);
assert!(data.maps.is_empty());
}
fn scratch(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("grimoire_project_data_{tag}_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap_or_else(|e| panic!("{e}"));
dir
}
#[test]
fn a_missing_map_names_its_path() {
let dir = scratch("missing_map");
let kits = Kits {
version: kits::KITS_VERSION,
note: None,
lattice: kits::Lattice::default(),
bind: Vec::new(),
bash: Vec::new(),
authoring: None,
};
std::fs::write(
dir.join(kits::KITS_FILE),
kits.to_ron().unwrap_or_else(|e| panic!("{e}")),
)
.unwrap_or_else(|e| panic!("{e}"));
let vocab = ron::to_string(&Vocabularies::default()).unwrap_or_else(|e| panic!("{e}"));
std::fs::write(dir.join(VOCAB_FILE), vocab).unwrap_or_else(|e| panic!("{e}"));
let loaded = load(&dir, &[]).unwrap_or_else(|e| panic!("an empty project loads: {e}"));
assert!(loaded.bound.library.descriptors.is_empty());
let err = load(&dir, &["town"]).expect_err("the map does not exist");
let want = dir.join("maps").join("town.map.ron");
assert!(
err.starts_with(&want.display().to_string()),
"the error must lead with the map's path {}: {err}",
want.display()
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_missing_kits_file_names_its_path() {
let dir = scratch("missing_kits");
let err = load(&dir, &[]).expect_err("no kits.ron");
assert!(err.starts_with(&dir.join(kits::KITS_FILE).display().to_string()), "{err}");
let _ = std::fs::remove_dir_all(&dir);
}
}