use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::{for_version, Attribute, Schema, SchemaVersion, TypeKind};
use crate::error::{ElementTypeError, ElementTypeResult};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Layout {
version: SchemaVersion,
schema: &'static Schema,
}
const fn proven(version: SchemaVersion) -> bool {
matches!(
version,
SchemaVersion::Ifc2x3 | SchemaVersion::Ifc4 | SchemaVersion::Ifc4x3
)
}
pub(crate) fn bind(model: &Model) -> ElementTypeResult<Layout> {
let version = match model.header().schema.as_slice() {
[] => SchemaVersion::Ifc4,
[token] => SchemaVersion::from_header_token(token)
.filter(|version| proven(*version))
.ok_or_else(|| ElementTypeError::UnsupportedSchema {
schema: token.clone(),
})?,
tokens => {
return Err(ElementTypeError::MultipleSchemas {
schemas: tokens.len(),
})
}
};
Layout::of(version)
}
impl Layout {
pub(crate) fn catalogue() -> ElementTypeResult<Self> {
Self::of(SchemaVersion::Ifc4x3)
}
fn of(version: SchemaVersion) -> ElementTypeResult<Self> {
let schema = for_version(version).map_err(|_| ElementTypeError::UnsupportedSchema {
schema: format!("{version:?}"),
})?;
Ok(Self { version, schema })
}
pub(crate) fn require_entity(self, entity: &'static str) -> ElementTypeResult<()> {
if self
.schema
.entity(entity)
.is_some_and(|found| !found.abstract_)
{
return Ok(());
}
Err(ElementTypeError::EntityNotInSchema {
entity,
schema: self.version,
})
}
pub(crate) fn attribute(self, entity: &str, attribute: &str) -> Option<&'static Attribute> {
self.schema
.attributes(entity)
.into_iter()
.find(|found| found.name.eq_ignore_ascii_case(attribute))
}
pub(crate) fn members(self, entity: &str, attribute: &str) -> Option<Vec<&'static str>> {
let declared = self.attribute(entity, attribute)?;
match &self.schema.type_def(&declared.type_name)?.kind {
TypeKind::Enumeration(members) => Some(members.iter().map(String::as_str).collect()),
_ => None,
}
}
pub(crate) fn version(self) -> SchemaVersion {
self.version
}
pub(crate) fn named_record(
self,
entity: &'static str,
values: Vec<(&'static str, Value)>,
) -> ElementTypeResult<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(ElementTypeError::AuthoringNotInSchema {
entity,
attribute,
schema: self.version,
})
}
}
}
for (declaration, value) in declared.iter().zip(&attributes) {
if *value == Value::Null && !declaration.optional {
return Err(ElementTypeError::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,
entity: &'static str,
id: EntityId,
) -> ElementTypeResult<()> {
let found = projected_type(tx, model, id).ok_or(ElementTypeError::MissingEntity { id })?;
if found.eq_ignore_ascii_case("IFCOWNERHISTORY") {
return Ok(());
}
Err(ElementTypeError::Invalid {
entity,
attribute: "OwnerHistory",
value: format!("#{} is a {found}, not an IfcOwnerHistory", id.0),
})
}
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())
}
#[cfg(test)]
mod intermediate_release_tests {
use super::*;
#[test]
fn ifc4x1_and_ifc4x2_are_refused_not_aliased() {
for token in ["IFC4X1", "IFC4X2"] {
let mut model = Model::new();
model.header_mut().schema = vec![token.to_owned()];
assert!(
matches!(bind(&model), Err(ElementTypeError::UnsupportedSchema { schema }) if schema == token),
"{token} must be refused"
);
}
}
}