use crate::metadata::EntityTypeMeta;
use crate::model_builder::{EntityConfig, ModelBuilder};
use crate::registration::{EntityConfigRegistration, EntityRegistration};
use std::any::TypeId;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub(crate) struct BuiltMetadata {
pub entity_metas: HashMap<TypeId, EntityTypeMeta>,
pub model_metas: Vec<EntityTypeMeta>,
pub configs: HashMap<TypeId, EntityConfig>,
}
pub(crate) struct MetadataCache {
by_key: RwLock<HashMap<Option<String>, Arc<BuiltMetadata>>>,
}
impl MetadataCache {
pub fn new() -> Self {
Self {
by_key: RwLock::new(HashMap::new()),
}
}
pub fn get_or_build(&self, context_key: Option<&str>) -> Arc<BuiltMetadata> {
let key = context_key.map(|s| s.to_string());
if let Ok(cache) = self.by_key.read() {
if let Some(built) = cache.get(&key) {
return Arc::clone(built);
}
}
let mut cache = match self.by_key.write() {
Ok(guard) => guard,
Err(poisoned) => {
let mut guard = poisoned.into_inner();
guard.clear();
guard
}
};
if let Some(existing) = cache.get(&key) {
return Arc::clone(existing);
}
let built = Arc::new(Self::build(context_key));
cache.insert(key, Arc::clone(&built));
built
}
fn build(context_key: Option<&str>) -> BuiltMetadata {
let mut model_builder = ModelBuilder::new();
for reg in inventory::iter::<EntityConfigRegistration> {
if reg.context_key == context_key {
(reg.apply_fn)(&mut model_builder);
}
}
let mut entity_metas: HashMap<TypeId, EntityTypeMeta> = HashMap::new();
for reg in inventory::iter::<EntityRegistration> {
if reg.context_key == context_key {
let meta = reg.meta();
let type_id = reg.type_id;
entity_metas.entry(type_id).or_insert_with(|| meta.clone());
if !model_builder.has_entity(type_id) {
model_builder.register_entity_meta(meta);
}
}
}
let model_metas: Vec<EntityTypeMeta> = model_builder.entity_metas_vec().to_vec();
let configs: HashMap<TypeId, EntityConfig> = model_builder.configs().clone();
BuiltMetadata {
entity_metas,
model_metas,
configs,
}
}
}
impl Default for MetadataCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_poison_recovery() {
let cache = MetadataCache::new();
let built1 = cache.get_or_build(None);
let first_entity_count = built1.entity_metas.len();
let first_model_count = built1.model_metas.len();
let _ = std::panic::catch_unwind(|| {
let _guard = cache.by_key.write().unwrap();
panic!("intentional poison for test");
});
let built2 = cache.get_or_build(None);
assert_eq!(
first_entity_count,
built2.entity_metas.len(),
"rebuilt metadata should have the same entity count as the first build"
);
assert_eq!(
first_model_count,
built2.model_metas.len(),
"rebuilt metadata should have the same model count as the first build"
);
}
#[test]
fn test_cache_hit_returns_same_arc() {
let cache = MetadataCache::new();
let built1 = cache.get_or_build(None);
let built2 = cache.get_or_build(None);
assert!(
Arc::ptr_eq(&built1, &built2),
"second call with same key should return the same cached Arc"
);
}
#[test]
fn test_different_context_keys_are_isolated() {
let cache = MetadataCache::new();
let built_default = cache.get_or_build(None);
let built_keyed = cache.get_or_build(Some("other"));
assert!(
!Arc::ptr_eq(&built_default, &built_keyed),
"different context_keys should produce independent BuiltMetadata"
);
}
}