use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::{for_version, Attribute, Schema, SchemaVersion, TypeKind};
use crate::error::ScheduleAuthoringError;
type Result<T> = std::result::Result<T, ScheduleAuthoringError>;
#[derive(Clone, Copy)]
pub(crate) struct Release {
version: SchemaVersion,
schema: &'static Schema,
}
pub(crate) fn bind(model: &Model) -> Result<Release> {
let version = match model.header().schema.as_slice() {
[] => SchemaVersion::Ifc4,
[token] => SchemaVersion::from_header_token(token).ok_or_else(|| {
ScheduleAuthoringError::UnsupportedSchema {
schema: token.clone(),
}
})?,
tokens => {
return Err(ScheduleAuthoringError::MultipleSchemas {
schemas: tokens.len(),
})
}
};
let schema = for_version(version).expect("every SchemaVersion has a bundled table");
Ok(Release { version, schema })
}
impl Release {
pub(crate) const fn version(self) -> SchemaVersion {
self.version
}
pub(crate) fn record(
self,
entity: &'static str,
values: Vec<(&'static str, Value)>,
) -> Result<Entity> {
if self.schema.entity(entity).is_none_or(|e| e.abstract_) {
return Err(ScheduleAuthoringError::EntityNotInSchema {
entity,
schema: self.version,
});
}
let declared = self.schema.attributes(entity);
let mut slots = vec![Value::Null; declared.len()];
for (attribute, value) in values {
let name = release_name(self.version, entity, attribute);
let Some(slot) = declared
.iter()
.position(|d| d.name.eq_ignore_ascii_case(name))
else {
if value == Value::Null {
continue;
}
return Err(ScheduleAuthoringError::AuthoringNotInSchema {
entity,
attribute,
schema: self.version,
});
};
if !conforms(self.schema, declared[slot], &value) {
return Err(ScheduleAuthoringError::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(ScheduleAuthoringError::AuthoringRequired {
entity,
attribute: missing.name.as_str(),
schema: self.version,
});
}
Ok(Entity::new(entity, slots))
}
pub(crate) fn require_owner_history(
self,
tx: &Transaction,
model: &Model,
entity: &'static str,
id: EntityId,
) -> Result<()> {
let actual =
projected_type(tx, model, id).ok_or(ScheduleAuthoringError::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(ScheduleAuthoringError::WrongReferenceType {
entity,
attribute: "OwnerHistory",
target: id,
actual,
expected: "IFCOWNERHISTORY",
})
}
}
}
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 release_name(release: SchemaVersion, entity: &str, attribute: &'static str) -> &'static str {
match (release, entity, attribute) {
(SchemaVersion::Ifc2x3, "IFCWORKPLAN" | "IFCWORKSCHEDULE", "Identification") => {
"Identifier"
}
(SchemaVersion::Ifc2x3, "IFCTASK", "Identification") => "TaskId",
(SchemaVersion::Ifc2x3, "IFCPROCEDURE", "Identification") => "ProcedureID",
(SchemaVersion::Ifc2x3, "IFCPROCEDURE", "PredefinedType") => "ProcedureType",
_ => attribute,
}
}
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,
}
}