use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::{for_version, Schema, SchemaVersion, TypeKind};
use crate::error::{OccurrenceError, OccurrenceResult};
use crate::table::Occurrence;
#[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) -> OccurrenceResult<Layout> {
let version = match model.header().schema.as_slice() {
[] => SchemaVersion::Ifc4,
[token] => SchemaVersion::from_header_token(token)
.filter(|version| proven(*version))
.ok_or_else(|| OccurrenceError::UnsupportedSchema {
schema: token.clone(),
})?,
tokens => {
return Err(OccurrenceError::MultipleSchemas {
schemas: tokens.len(),
})
}
};
let schema = for_version(version).map_err(|_| 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) const fn version(self) -> SchemaVersion {
self.version
}
pub(crate) fn members(self, entity: &str, attribute: &str) -> Option<Vec<&'static str>> {
let declared = self
.schema
.attributes(entity)
.into_iter()
.find(|found| found.name.eq_ignore_ascii_case(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 is_a(self, found: &str, expected: &str) -> bool {
self.schema.is_a(found, expected)
}
pub(crate) fn type_class(self, kind: Occurrence) -> OccurrenceResult<Option<&'static str>> {
#[allow(unreachable_patterns)]
match self.version {
SchemaVersion::Ifc2x3 => Ok(kind.ifc2x3_type_class),
SchemaVersion::Ifc4 => Ok(kind.ifc4_type_class),
SchemaVersion::Ifc4x3 => Ok(kind.type_class),
other => Err(OccurrenceError::UnsupportedSchema {
schema: format!("{other:?}"),
}),
}
}
}
impl Layout {
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())
}
#[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(OccurrenceError::UnsupportedSchema { schema }) if schema == token),
"{token} must be refused"
);
}
}
}