use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::{for_version, Schema, SchemaVersion, TypeKind};
use crate::authoring::{OccurrenceError, OccurrenceResult};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Layout {
version: SchemaVersion,
schema: &'static Schema,
}
pub(crate) fn bind(model: &Model) -> OccurrenceResult<Layout> {
let version = match model.header().schema.as_slice() {
[] => SchemaVersion::Ifc4,
[token] => SchemaVersion::from_header_token(token).ok_or_else(|| {
OccurrenceError::UnsupportedSchema {
schema: token.clone(),
}
})?,
tokens => {
return Err(OccurrenceError::MultipleSchemas {
schemas: tokens.len(),
})
}
};
let schema = for_version(version).ok_or_else(|| OccurrenceError::UnsupportedSchema {
schema: format!("{version:?}"),
})?;
Ok(Layout { version, schema })
}
impl Layout {
pub(crate) fn require_entity(self, entity: &'static str) -> OccurrenceResult<()> {
if self
.schema
.entity(entity)
.is_some_and(|found| !found.abstract_)
{
return Ok(());
}
Err(OccurrenceError::EntityNotInSchema {
entity,
schema: self.version,
})
}
pub(crate) fn predefined_members(self, entity: &str) -> Option<Vec<&'static str>> {
let declared = self
.schema
.attributes(entity)
.into_iter()
.find(|attribute| attribute.name.eq_ignore_ascii_case("PredefinedType"))?;
match &self.schema.type_def(&declared.type_name)?.kind {
TypeKind::Enumeration(members) => Some(members.iter().map(String::as_str).collect()),
_ => None,
}
}
pub(crate) fn named_record(
self,
entity: &'static str,
values: Vec<(&'static str, Value)>,
) -> OccurrenceResult<Entity> {
let declared = self.schema.attributes(entity);
let mut attributes = vec![Value::Null; declared.len()];
for (attribute, value) in values {
let slot = declared
.iter()
.position(|found| found.name.eq_ignore_ascii_case(attribute));
match slot {
Some(slot) => attributes[slot] = value,
None if value == Value::Null => {}
None => {
return Err(OccurrenceError::AuthoringNotInSchema {
entity,
attribute,
schema: self.version,
})
}
}
}
for (declaration, value) in declared.iter().zip(&attributes) {
if *value == Value::Null && !declaration.optional {
return Err(OccurrenceError::AuthoringRequired {
entity,
attribute: declaration.name.as_str(),
schema: self.version,
});
}
}
Ok(Entity::new(entity, attributes))
}
}
pub(crate) fn require_owner_history(
tx: &Transaction,
model: &Model,
id: EntityId,
) -> OccurrenceResult<()> {
let found =
projected_type(tx, model, id).ok_or(OccurrenceError::UnresolvedOwnerHistory { id })?;
if found.eq_ignore_ascii_case("IFCOWNERHISTORY") {
return Ok(());
}
Err(OccurrenceError::NotAnOwnerHistory { id, found })
}
fn projected_type(tx: &Transaction, model: &Model, id: EntityId) -> Option<String> {
for edit in tx.edits().iter().rev() {
match edit {
Edit::Create {
id: edit_id,
entity,
} if *edit_id == id => return Some(entity.type_name.to_string()),
Edit::Remove { id: edit_id } if *edit_id == id => return None,
Edit::Retype {
id: edit_id,
type_name,
} if *edit_id == id => return Some(type_name.to_string()),
_ => {}
}
}
model.get(id).map(|entity| entity.type_name.to_string())
}