use std::sync::Arc;
use indexmap::IndexMap;
use murk_core::{FieldDef, FieldId, FieldMutability};
use crate::error::ArenaError;
use crate::handle::{FieldHandle, FieldLocation};
#[derive(Clone, Debug)]
pub struct FieldMeta {
pub components: u32,
pub mutability: FieldMutability,
pub total_len: u32,
pub name: Arc<str>,
}
#[derive(Clone, Debug)]
pub struct FieldEntry {
pub handle: FieldHandle,
pub meta: FieldMeta,
}
#[derive(Clone, Debug)]
pub struct FieldDescriptor {
entries: IndexMap<FieldId, FieldEntry>,
}
impl FieldDescriptor {
pub fn from_field_defs(
field_defs: &[(FieldId, FieldDef)],
cell_count: u32,
) -> Result<Self, ArenaError> {
let mut entries = IndexMap::with_capacity(field_defs.len());
for (id, def) in field_defs {
let components = def.field_type.components();
let total_len = cell_count.checked_mul(components).ok_or(ArenaError::InvalidConfig {
reason: format!(
"cell_count ({cell_count}) * components ({components}) overflows u32 for field '{}'",
def.name,
),
})?;
let meta = FieldMeta {
components,
mutability: def.mutability,
total_len,
name: Arc::from(def.name.as_str()),
};
let handle =
FieldHandle::new(0, 0, total_len, FieldLocation::PerTick { segment_index: 0 });
entries.insert(*id, FieldEntry { handle, meta });
}
Ok(Self { entries })
}
pub fn get(&self, field: FieldId) -> Option<&FieldEntry> {
self.entries.get(&field)
}
pub fn update_handle(&mut self, field: FieldId, handle: FieldHandle) {
if let Some(entry) = self.entries.get_mut(&field) {
entry.handle = handle;
}
}
pub fn iter(&self) -> impl Iterator<Item = (&FieldId, &FieldEntry)> {
self.entries.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&FieldId, &mut FieldEntry)> {
self.entries.iter_mut()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn fields_by_mutability(
&self,
mutability: FieldMutability,
) -> impl Iterator<Item = (&FieldId, &FieldEntry)> {
self.entries
.iter()
.filter(move |(_, entry)| entry.meta.mutability == mutability)
}
}
#[cfg(test)]
mod tests {
use super::*;
use murk_core::{BoundaryBehavior, FieldType};
fn make_field_defs() -> Vec<(FieldId, FieldDef)> {
vec![
(
FieldId(0),
FieldDef {
name: "temperature".to_string(),
field_type: FieldType::Scalar,
mutability: FieldMutability::PerTick,
units: None,
bounds: None,
boundary_behavior: BoundaryBehavior::Clamp,
},
),
(
FieldId(1),
FieldDef {
name: "velocity".to_string(),
field_type: FieldType::Vector { dims: 3 },
mutability: FieldMutability::PerTick,
units: None,
bounds: None,
boundary_behavior: BoundaryBehavior::Clamp,
},
),
(
FieldId(2),
FieldDef {
name: "terrain".to_string(),
field_type: FieldType::Categorical { n_values: 4 },
mutability: FieldMutability::Static,
units: None,
bounds: None,
boundary_behavior: BoundaryBehavior::Clamp,
},
),
(
FieldId(3),
FieldDef {
name: "resources".to_string(),
field_type: FieldType::Scalar,
mutability: FieldMutability::Sparse,
units: None,
bounds: Some((0.0, 100.0)),
boundary_behavior: BoundaryBehavior::Clamp,
},
),
]
}
#[test]
fn from_field_defs_creates_entries() {
let defs = make_field_defs();
let desc = FieldDescriptor::from_field_defs(&defs, 100).unwrap();
assert_eq!(desc.len(), 4);
}
#[test]
fn total_len_is_cell_count_times_components() {
let defs = make_field_defs();
let desc = FieldDescriptor::from_field_defs(&defs, 100).unwrap();
assert_eq!(desc.get(FieldId(0)).unwrap().meta.total_len, 100);
assert_eq!(desc.get(FieldId(1)).unwrap().meta.total_len, 300);
assert_eq!(desc.get(FieldId(2)).unwrap().meta.total_len, 100);
}
#[test]
fn update_handle_changes_handle() {
let defs = make_field_defs();
let mut desc = FieldDescriptor::from_field_defs(&defs, 100).unwrap();
let new_handle =
FieldHandle::new(5, 1024, 100, FieldLocation::PerTick { segment_index: 2 });
desc.update_handle(FieldId(0), new_handle);
let entry = desc.get(FieldId(0)).unwrap();
assert_eq!(entry.handle.generation(), 5);
assert_eq!(
entry.handle.location(),
FieldLocation::PerTick { segment_index: 2 }
);
}
#[test]
fn fields_by_mutability_filters() {
let defs = make_field_defs();
let desc = FieldDescriptor::from_field_defs(&defs, 100).unwrap();
let per_tick: Vec<_> = desc
.fields_by_mutability(FieldMutability::PerTick)
.collect();
assert_eq!(per_tick.len(), 2);
let static_fields: Vec<_> = desc.fields_by_mutability(FieldMutability::Static).collect();
assert_eq!(static_fields.len(), 1);
let sparse: Vec<_> = desc.fields_by_mutability(FieldMutability::Sparse).collect();
assert_eq!(sparse.len(), 1);
}
#[test]
fn unknown_field_returns_none() {
let defs = make_field_defs();
let desc = FieldDescriptor::from_field_defs(&defs, 100).unwrap();
assert!(desc.get(FieldId(99)).is_none());
}
#[test]
fn overflow_cell_count_times_components_returns_error() {
let defs = vec![(
FieldId(0),
FieldDef {
name: "huge".to_string(),
field_type: FieldType::Vector { dims: u32::MAX },
mutability: FieldMutability::PerTick,
units: None,
bounds: None,
boundary_behavior: BoundaryBehavior::Clamp,
},
)];
let result = FieldDescriptor::from_field_defs(&defs, u32::MAX);
assert!(matches!(result, Err(ArenaError::InvalidConfig { .. })));
}
#[cfg(not(miri))]
mod proptests {
use super::*;
use murk_core::{BoundaryBehavior, FieldType};
use proptest::prelude::*;
fn arb_field_type() -> impl Strategy<Value = FieldType> {
prop_oneof![
Just(FieldType::Scalar),
(2u32..8).prop_map(|d| FieldType::Vector { dims: d }),
(2u32..16).prop_map(|n| FieldType::Categorical { n_values: n }),
]
}
proptest! {
#[test]
fn total_len_equals_cell_count_times_components(
cell_count in 1u32..1000,
field_type in arb_field_type(),
) {
let components = field_type.components();
let defs = vec![(
FieldId(0),
FieldDef {
name: "f".into(),
field_type,
mutability: FieldMutability::PerTick,
units: None,
bounds: None,
boundary_behavior: BoundaryBehavior::Clamp,
},
)];
let desc = FieldDescriptor::from_field_defs(&defs, cell_count).unwrap();
let entry = desc.get(FieldId(0)).unwrap();
prop_assert_eq!(
entry.meta.total_len,
cell_count * components
);
}
#[test]
fn len_equals_number_of_field_defs(
n_fields in 1usize..20,
) {
let defs: Vec<_> = (0..n_fields)
.map(|i| (
FieldId(i as u32),
FieldDef {
name: format!("f{i}"),
field_type: FieldType::Scalar,
mutability: FieldMutability::PerTick,
units: None,
bounds: None,
boundary_behavior: BoundaryBehavior::Clamp,
},
))
.collect();
let desc = FieldDescriptor::from_field_defs(&defs, 100).unwrap();
prop_assert_eq!(desc.len(), n_fields);
prop_assert_eq!(desc.is_empty(), n_fields == 0);
}
}
}
}