use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::{for_version, Schema, SchemaVersion};
use crate::{ConstraintError, ConstraintResult};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Layout {
version: SchemaVersion,
schema: &'static Schema,
}
pub(crate) fn bind(model: &Model) -> ConstraintResult<Layout> {
let version = match model.header().schema.as_slice() {
[] => SchemaVersion::Ifc4,
[token] => SchemaVersion::from_header_token(token).ok_or_else(|| {
ConstraintError::UnsupportedSchema {
schema: token.clone(),
}
})?,
tokens => {
return Err(ConstraintError::MultipleSchemas {
schemas: tokens.len(),
})
}
};
let schema = for_version(version).ok_or_else(|| ConstraintError::UnsupportedSchema {
schema: format!("{version:?}"),
})?;
Ok(Layout { version, schema })
}
impl Layout {
pub(crate) fn schema(self) -> &'static Schema {
self.schema
}
pub(crate) fn is_definition(self, actual: &str) -> bool {
if self.schema.type_def("IfcDefinitionSelect").is_some() {
return self.schema.accepts_type("IfcDefinitionSelect", actual);
}
self.schema.is_a(actual, "IFCOBJECTDEFINITION")
|| self.schema.is_a(actual, "IFCPROPERTYDEFINITION")
}
pub(crate) fn named_record(
self,
entity: &'static str,
values: Vec<(&'static str, Value)>,
) -> ConstraintResult<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(ConstraintError::AuthoringInvalid {
entity,
attribute,
value: format!("not declared by {:?}", self.version),
})
}
}
}
for (declaration, value) in declared.iter().zip(&attributes) {
if *value == Value::Null && !declaration.optional {
return Err(ConstraintError::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,
) -> ConstraintResult<()> {
let actual = projected_type(tx, model, id).ok_or(ConstraintError::UnknownEntity { id })?;
if actual.eq_ignore_ascii_case("IFCOWNERHISTORY") {
return Ok(());
}
Err(ConstraintError::AuthoringReferenceType {
target: id,
expected: "IfcOwnerHistory",
actual,
})
}
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())
}