use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::{for_version, Attribute, Schema, SchemaVersion, TypeKind};
use super::{SystemAuthoringError, SystemAuthoringResult};
#[derive(Clone, Copy)]
pub(super) struct Release {
version: SchemaVersion,
schema: &'static Schema,
}
pub(super) fn bind(model: &Model) -> SystemAuthoringResult<Release> {
let version = match model.header().schema.as_slice() {
[] => SchemaVersion::Ifc4,
[token] => SchemaVersion::from_header_token(token).ok_or_else(|| {
SystemAuthoringError::UnsupportedSchema {
schema: token.clone(),
}
})?,
tokens => {
return Err(SystemAuthoringError::MultipleSchemas {
schemas: tokens.len(),
})
}
};
let schema = for_version(version).expect("every SchemaVersion has a bundled table");
Ok(Release { version, schema })
}
impl Release {
pub(super) const fn schema(self) -> &'static Schema {
self.schema
}
pub(super) fn require_entity(self, entity: &'static str) -> SystemAuthoringResult<()> {
if self.schema.entity(entity).is_some_and(|e| !e.abstract_) {
Ok(())
} else {
Err(SystemAuthoringError::EntityNotInSchema {
entity,
schema: self.version,
})
}
}
pub(super) fn record(
self,
entity: &'static str,
values: Vec<(&'static str, Value)>,
) -> SystemAuthoringResult<Entity> {
self.require_entity(entity)?;
let declared = self.schema.attributes(entity);
let mut slots = vec![Value::Null; declared.len()];
for (attribute, value) in values {
let Some(slot) = declared
.iter()
.position(|d| d.name.eq_ignore_ascii_case(attribute))
else {
if value == Value::Null {
continue;
}
return Err(SystemAuthoringError::AuthoringNotInSchema {
entity,
attribute,
schema: self.version,
});
};
if !conforms(self.schema, declared[slot], &value) {
return Err(SystemAuthoringError::AuthoringValueType {
entity,
attribute,
declared: declared[slot].type_name.as_str(),
schema: self.version,
});
}
slots[slot] = value;
}
if let Some(missing) = declared
.iter()
.zip(&slots)
.find(|(d, value)| !d.optional && **value == Value::Null)
{
return Err(SystemAuthoringError::AuthoringRequired {
entity,
attribute: missing.0.name.as_str(),
schema: self.version,
});
}
Ok(Entity::new(entity, slots))
}
pub(super) fn require_owner_history(
self,
tx: &Transaction,
model: &Model,
entity: &'static str,
id: EntityId,
) -> SystemAuthoringResult<()> {
let actual =
projected_type(tx, model, id).ok_or(SystemAuthoringError::MissingReference {
entity,
attribute: "OwnerHistory",
target: id,
})?;
let accepted = self
.schema
.attributes(entity)
.iter()
.find(|d| d.name.eq_ignore_ascii_case("OwnerHistory"))
.is_some_and(|d| self.schema.accepts_type(&d.type_name, &actual));
if accepted {
Ok(())
} else {
Err(SystemAuthoringError::WrongReferenceType {
entity,
attribute: "OwnerHistory",
target: id,
actual,
expected: "IFCOWNERHISTORY",
})
}
}
pub(super) fn stage(
self,
tx: &mut Transaction,
model: &Model,
entity: &'static str,
mut values: Vec<(&'static str, Value)>,
owner_history: EntityId,
) -> SystemAuthoringResult<EntityId> {
values.push(("OwnerHistory", Value::Ref(owner_history)));
let record = self.record(entity, values)?;
self.require_owner_history(tx, model, entity, owner_history)?;
Ok(tx.create(record))
}
}
fn projected_type(tx: &Transaction, model: &Model, target: EntityId) -> Option<String> {
for edit in tx.edits().iter().rev() {
match edit {
Edit::Remove { id } if *id == target => return None,
Edit::Retype { id, type_name } if *id == target => return Some(type_name.to_string()),
Edit::Create { id, entity } if *id == target => {
return Some(entity.type_name.to_string())
}
_ => {}
}
}
model.get(target).map(|entity| entity.type_name.to_string())
}
fn conforms(schema: &Schema, declared: &Attribute, value: &Value) -> bool {
match value {
Value::Null => true,
Value::List(items) => {
(declared.aggregate || is_aggregate(schema, &declared.type_name))
&& items
.iter()
.all(|item| scalar(schema, &declared.type_name, item))
}
_ => !declared.aggregate && scalar(schema, &declared.type_name, value),
}
}
fn is_aggregate(schema: &Schema, declared: &str) -> bool {
let base = schema.resolve_defined(declared).to_ascii_uppercase();
["LIST", "SET", "BAG", "ARRAY"]
.iter()
.any(|kind| base.starts_with(kind))
}
fn scalar(schema: &Schema, declared: &str, value: &Value) -> bool {
let base = schema.resolve_defined(declared).to_ascii_uppercase();
let kind = schema.type_def(declared).map(|t| &t.kind);
match value {
Value::Text(_) => base.starts_with("STRING"),
Value::Enum(token) => matches!(kind, Some(TypeKind::Enumeration(members))
if members.iter().any(|m| m.eq_ignore_ascii_case(token))),
Value::Bool(_) => base == "BOOLEAN" || base == "LOGICAL",
Value::LogicalUnknown => base == "LOGICAL",
Value::Integer(_) => base == "INTEGER" || base == "NUMBER",
Value::Real(_) => base.starts_with("REAL") || base == "NUMBER",
Value::Ref(_) => admits_entity(schema, declared, 8),
Value::Typed { type_name, .. } => {
matches!(kind, Some(TypeKind::Select(_))) && schema.accepts_type(declared, type_name)
}
_ => false,
}
}
fn admits_entity(schema: &Schema, declared: &str, depth: usize) -> bool {
if schema.entity(declared).is_some() {
return true;
}
match schema.type_def(declared).map(|t| &t.kind) {
Some(TypeKind::Select(members)) if depth > 0 => members
.iter()
.any(|member| admits_entity(schema, member, depth - 1)),
_ => false,
}
}