use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::{for_version, Attribute, Schema, SchemaVersion, TypeKind};
use crate::{ConstraintError, ConstraintResult};
#[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
)
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Binding<'m> {
Bound(Layout),
Multiple(usize),
Unsupported(&'m str),
}
impl<'m> Binding<'m> {
pub(crate) fn of(model: &'m Model) -> Self {
match model.header().schema.as_slice() {
[] => Layout::of_version(SchemaVersion::Ifc4)
.map_or(Self::Unsupported("IFC4"), Self::Bound),
[token] => SchemaVersion::from_header_token(token)
.and_then(|version| Layout::of_version(version).ok())
.map_or(Self::Unsupported(token.as_str()), Self::Bound),
tokens => Self::Multiple(tokens.len()),
}
}
pub(crate) fn layout(self) -> ConstraintResult<Layout> {
match self {
Self::Bound(layout) => Ok(layout),
Self::Multiple(schemas) => Err(ConstraintError::MultipleSchemas { schemas }),
Self::Unsupported(schema) => Err(ConstraintError::UnsupportedSchema {
schema: schema.to_owned(),
}),
}
}
}
pub(crate) fn bind(model: &Model) -> ConstraintResult<Layout> {
Binding::of(model).layout()
}
impl Layout {
pub(crate) fn of_version(version: SchemaVersion) -> ConstraintResult<Self> {
let unsupported = || ConstraintError::UnsupportedSchema {
schema: version.release_id().to_owned(),
};
if !proven(version) {
return Err(unsupported());
}
let schema = for_version(version).map_err(|_| unsupported())?;
Ok(Self { version, schema })
}
pub(crate) const fn version(self) -> SchemaVersion {
self.version
}
pub(crate) fn schema(self) -> &'static Schema {
self.schema
}
pub(crate) fn require_entity(self, entity: &'static str) -> ConstraintResult<()> {
if self.schema.entity(entity).is_some_and(|e| !e.abstract_) {
Ok(())
} else {
Err(ConstraintError::EntityNotInSchema {
entity,
schema: self.version,
})
}
}
pub(crate) fn declared(
self,
entity: &str,
attribute: &'static str,
) -> Option<(usize, &'static Attribute)> {
self.schema
.attributes(entity)
.into_iter()
.enumerate()
.find(|(_, declared)| declared.name.eq_ignore_ascii_case(attribute))
}
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> {
self.require_entity(entity)?;
let declared = self.schema.attributes(entity);
let mut attributes = vec![Value::Null; declared.len()];
for (attribute, value) in values {
let Some((slot, declaration)) = self.declared(entity, attribute) else {
if value == Value::Null {
continue;
}
return Err(ConstraintError::AuthoringNotInSchema {
entity,
attribute,
schema: self.version,
});
};
if !self.holds(declaration, &value) {
return Err(ConstraintError::AuthoringValueType {
entity,
attribute,
declared: declaration.type_name.as_str(),
schema: self.version,
});
}
attributes[slot] = value;
}
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))
}
fn holds(self, declaration: &Attribute, value: &Value) -> bool {
match value {
Value::Null => true,
Value::List(items) => {
declaration.aggregate
&& items
.iter()
.all(|item| self.holds_scalar(&declaration.type_name, item))
}
other => !declaration.aggregate && self.holds_scalar(&declaration.type_name, other),
}
}
fn holds_scalar(self, declared: &str, value: &Value) -> bool {
match value {
Value::Text(_) => self.is_text(declared),
Value::Ref(_) => self.admits_entity(declared, 8),
_ => true,
}
}
pub(crate) fn is_text(self, declared: &str) -> bool {
self.schema
.resolve_defined(declared)
.to_ascii_uppercase()
.starts_with("STRING")
}
pub(crate) fn admits_entity(self, declared: &str, depth: usize) -> bool {
if self.schema.entity(declared).is_some() {
return true;
}
match self.schema.type_def(declared).map(|t| &t.kind) {
Some(TypeKind::Select(members)) if depth > 0 => members
.iter()
.any(|member| self.admits_entity(member, depth - 1)),
_ => false,
}
}
}
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())
}
#[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(ConstraintError::UnsupportedSchema { schema }) if schema == token),
"{token} must be refused"
);
}
}
}