use std::cell::RefCell;
use std::sync::Once;
use super::name_interner::NameInterner;
pub use concinnity_core::ecs::asset_id::{
AssetId, AssetRef, de_opt_asset_ref, de_opt_asset_ref_typed,
};
thread_local! {
static INTERNER: RefCell<NameInterner> = RefCell::new(NameInterner::default());
}
pub fn ensure_name_resolver() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
concinnity_core::ecs::resolver::set_name_resolver(|name| {
INTERNER.with(|i| i.borrow_mut().intern(name))
});
});
}
pub fn intern(name: &str) -> AssetId {
ensure_name_resolver();
AssetId(INTERNER.with(|i| i.borrow_mut().intern(name)))
}
pub fn reset_interner() {
ensure_name_resolver();
INTERNER.with(|i| *i.borrow_mut() = NameInterner::default());
}
pub fn lookup(name: &str) -> Option<AssetId> {
ensure_name_resolver();
INTERNER.with(|i| i.borrow().lookup(name).map(AssetId))
}
pub fn name_of(id: AssetId) -> Option<String> {
ensure_name_resolver();
INTERNER.with(|i| match i.borrow().name(id.0) {
"" => None,
name => Some(name.to_string()),
})
}
pub fn name_table() -> Vec<String> {
INTERNER.with(|i| i.borrow().names().map(str::to_string).collect())
}
pub fn prime_name_table(pairs: &[(u32, String)]) -> bool {
ensure_name_resolver();
INTERNER.with(|i| i.borrow_mut().prime(pairs))
}
pub fn intern_all(names: &[&str]) {
ensure_name_resolver();
INTERNER.with(|i| {
let mut interner = i.borrow_mut();
for n in names {
interner.intern(n);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intern_is_idempotent_and_dense() {
reset_interner();
intern_all(&["a", "b", "c"]);
assert_eq!(intern("a"), AssetId(0));
assert_eq!(intern("b"), AssetId(1));
assert_eq!(intern("c"), AssetId(2));
assert_eq!(intern("d"), AssetId(3));
assert_eq!(intern("a"), AssetId(0));
}
#[test]
fn name_table_snapshots_in_id_order() {
reset_interner();
intern_all(&["a", "b", "c"]);
assert_eq!(name_table(), vec!["a", "b", "c"]);
}
#[test]
fn name_of_resolves_one_id_and_reports_unnamed_slots_as_absent() {
reset_interner();
intern_all(&["floor", "wall"]);
assert_eq!(name_of(AssetId(0)).as_deref(), Some("floor"));
assert_eq!(name_of(AssetId(1)).as_deref(), Some("wall"));
assert_eq!(name_of(AssetId(7)), None, "an id past the table is absent");
reset_interner();
prime_name_table(&[(0, "floor".to_string()), (2, "lamp".to_string())]);
assert_eq!(name_of(AssetId(1)), None, "a blank slot is absent");
assert_eq!(name_of(AssetId(2)).as_deref(), Some("lamp"));
}
#[test]
fn lookup_agrees_with_scanning_a_name_table_snapshot() {
let scan = |name: &str| {
name_table()
.iter()
.position(|n| n == name)
.map(|i| AssetId(i as u32))
};
reset_interner();
intern_all(&["floor", "wall", "lamp"]);
for name in ["floor", "wall", "lamp", "missing"] {
assert_eq!(lookup(name), scan(name), "disagreed on '{name}'");
}
reset_interner();
prime_name_table(&[(0, "floor".to_string()), (2, "lamp".to_string())]);
for name in ["floor", "lamp", "missing"] {
assert_eq!(lookup(name), scan(name), "disagreed on '{name}'");
}
assert_eq!(scan(""), Some(AssetId(1)));
assert_eq!(lookup(""), None);
}
#[test]
fn lookup_resolves_interned_names_only() {
reset_interner();
intern_all(&["a", "b"]);
assert_eq!(lookup("a"), Some(AssetId(0)));
assert_eq!(lookup("b"), Some(AssetId(1)));
assert_eq!(lookup("c"), None);
assert_eq!(name_table().len(), 2);
reset_interner();
prime_name_table(&[(0, "floor".to_string()), (2, "lamp".to_string())]);
assert_eq!(name_table()[1], "");
assert_eq!(lookup(""), None);
assert_eq!(lookup("lamp"), Some(AssetId(2)));
}
#[test]
fn prime_installs_a_sparse_table_and_yields_to_a_cook() {
reset_interner();
assert!(prime_name_table(&[
(0, "floor".to_string()),
(2, "lamp".to_string())
]));
assert_eq!(intern("floor"), AssetId(0));
assert_eq!(intern("lamp"), AssetId(2));
assert_eq!(name_table()[1], "", "unrecorded slot stays blank");
assert_eq!(intern("new"), AssetId(3));
assert!(!prime_name_table(&[(0, "other".to_string())]));
assert_eq!(intern("floor"), AssetId(0));
}
#[test]
fn asset_id_deserializes_from_name_string_via_the_seam() {
reset_interner();
intern_all(&["floor", "wall"]);
let id: AssetId = serde_json::from_str("\"wall\"").unwrap();
assert_eq!(id, AssetId(1));
}
#[test]
fn opt_ref_resolves_a_name_and_treats_empty_as_none() {
reset_interner();
intern_all(&["mesh_a"]);
#[derive(serde::Deserialize)]
struct Holder {
#[serde(default, deserialize_with = "de_opt_asset_ref")]
r: Option<AssetId>,
}
let named: Holder = serde_json::from_str("{\"r\":\"mesh_a\"}").unwrap();
assert_eq!(named.r, Some(AssetId(0)));
let empty: Holder = serde_json::from_str("{\"r\":\"\"}").unwrap();
assert_eq!(empty.r, None);
let by_id: Holder = serde_json::from_str("{\"r\":5}").unwrap();
assert_eq!(by_id.r, Some(AssetId(5)));
}
}