use ifc_model::{Entity, EntityId, Model, Value};
use ifc_schema::{for_version, Attribute, Schema, SchemaVersion, TypeKind};
use crate::{ClassificationError, ClassificationResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Release<'m> {
Bound(SchemaVersion),
Multiple(usize),
Unsupported(&'m str),
}
impl<'m> Release<'m> {
pub(crate) const LEGACY: Release<'static> = Release::Bound(SchemaVersion::Ifc4);
pub(crate) fn of(model: &'m Model) -> Self {
match model.header().schema.as_slice() {
[] => Release::LEGACY,
[token] => SchemaVersion::from_header_token(token)
.map_or(Self::Unsupported(token.as_str()), Self::Bound),
tokens => Self::Multiple(tokens.len()),
}
}
pub(crate) fn bound(self) -> ClassificationResult<(SchemaVersion, &'static Schema)> {
match self {
Self::Bound(version) => Ok((
version,
for_version(version).expect("every SchemaVersion has a bundled table"),
)),
Self::Multiple(schemas) => Err(ClassificationError::MultipleSchemas { schemas }),
Self::Unsupported(schema) => Err(ClassificationError::UnsupportedSchema {
schema: schema.to_owned(),
}),
}
}
pub(crate) fn slot(
self,
entity: &'static str,
id: EntityId,
attribute: &'static str,
) -> ClassificationResult<usize> {
let (version, schema) = self.bound()?;
position(schema, version, entity, attribute)
.map(|(slot, _)| slot)
.ok_or(ClassificationError::NotInSchema {
entity,
id,
attribute,
schema: version,
})
}
pub(crate) fn text_slot(
self,
entity: &'static str,
id: EntityId,
record: &Entity,
attribute: &'static str,
) -> ClassificationResult<usize> {
let slot = self.slot(entity, id, attribute)?;
if let Some(Value::Ref(target)) = record.attribute(slot) {
let declared = self.declared_type(entity, id, attribute)?;
let (_, schema) = self.bound()?;
if schema.entity(declared).is_some() {
return Err(ClassificationError::StructuredValue {
entity,
id,
attribute,
target: *target,
});
}
}
Ok(slot)
}
pub(crate) fn declared_type(
self,
entity: &'static str,
id: EntityId,
attribute: &'static str,
) -> ClassificationResult<&'static str> {
let slot = self.slot(entity, id, attribute)?;
let (_, schema) = self.bound()?;
Ok(schema.attributes(entity)[slot].type_name.as_str())
}
pub(crate) fn accepts(
self,
entity: &'static str,
id: EntityId,
attribute: &'static str,
candidate: &str,
) -> ClassificationResult<bool> {
let declared = self.declared_type(entity, id, attribute)?;
let (_, schema) = self.bound()?;
Ok(schema.accepts_type(declared, candidate))
}
pub(crate) fn enumerators(
self,
entity: &'static str,
id: EntityId,
attribute: &'static str,
) -> ClassificationResult<Vec<&'static str>> {
let declared = self.declared_type(entity, id, attribute)?;
let (_, schema) = self.bound()?;
Ok(enumeration(schema, declared))
}
pub(crate) fn require_entity(
self,
entity: &'static str,
) -> ClassificationResult<(SchemaVersion, &'static Schema)> {
let (version, schema) = self.bound()?;
if schema.entity(entity).is_some_and(|e| !e.abstract_) {
Ok((version, schema))
} else {
Err(ClassificationError::EntityNotInSchema {
entity,
schema: version,
})
}
}
pub(crate) fn declared(
self,
entity: &'static str,
attribute: &'static str,
) -> ClassificationResult<&'static Attribute> {
let (version, schema) = self.require_entity(entity)?;
position(schema, version, entity, attribute)
.map(|(_, declared)| declared)
.ok_or(ClassificationError::AuthoringNotInSchema {
entity,
attribute,
schema: version,
})
}
pub(crate) fn record(
self,
entity: &'static str,
values: Vec<(&'static str, Value)>,
) -> ClassificationResult<Entity> {
let (version, schema) = self.require_entity(entity)?;
let declared = schema.attributes(entity);
let mut slots = vec![None; declared.len()];
for (attribute, value) in values {
match position(schema, version, entity, attribute) {
Some((slot, declaration)) => {
if holds_text(&value) && !is_text_type(schema, &declaration.type_name) {
return Err(ClassificationError::AuthoringValueType {
entity,
attribute,
declared: declaration.type_name.as_str(),
schema: version,
});
}
slots[slot] = Some(value);
}
None if value == Value::Null => {}
None => {
return Err(ClassificationError::AuthoringNotInSchema {
entity,
attribute,
schema: version,
})
}
}
}
let mut attributes = Vec::with_capacity(slots.len());
for (slot, value) in slots.into_iter().enumerate() {
match value.unwrap_or(Value::Null) {
Value::Null if !declared[slot].optional => {
return Err(ClassificationError::AuthoringRequired {
entity,
attribute: declared[slot].name.as_str(),
schema: version,
})
}
value => attributes.push(value),
}
}
Ok(Entity::new(entity, attributes))
}
}
fn position(
schema: &'static Schema,
version: SchemaVersion,
entity: &str,
attribute: &'static str,
) -> Option<(usize, &'static Attribute)> {
let name = release_name(version, entity, attribute);
schema
.attributes(entity)
.into_iter()
.enumerate()
.find(|(_, declared)| declared.name.eq_ignore_ascii_case(name))
}
fn holds_text(value: &Value) -> bool {
match value {
Value::Text(_) => true,
Value::List(items) => items.iter().any(holds_text),
_ => false,
}
}
fn is_text_type(schema: &Schema, declared: &str) -> bool {
schema
.resolve_defined(declared)
.to_ascii_uppercase()
.starts_with("STRING")
}
pub(crate) fn enumeration(schema: &'static Schema, declared: &str) -> Vec<&'static str> {
match schema.type_def(declared).map(|definition| &definition.kind) {
Some(TypeKind::Enumeration(values)) => values.iter().map(String::as_str).collect(),
_ => Vec::new(),
}
}
pub(crate) fn release_name(
release: SchemaVersion,
entity: &str,
attribute: &'static str,
) -> &'static str {
let entity = entity.to_ascii_uppercase();
match (release, entity.as_str(), attribute) {
(SchemaVersion::Ifc2x3, "IFCDOCUMENTINFORMATION", "Identification") => "DocumentId",
(
SchemaVersion::Ifc2x3,
"IFCCLASSIFICATIONREFERENCE" | "IFCDOCUMENTREFERENCE" | "IFCLIBRARYREFERENCE",
"Identification",
) => "ItemReference",
(SchemaVersion::Ifc4x3, "IFCCLASSIFICATION", "Location") => "Specification",
_ => attribute,
}
}
pub fn classification_schema(model: &Model) -> ClassificationResult<SchemaVersion> {
Release::of(model).bound().map(|(version, _)| version)
}
#[cfg(test)]
mod binding_tests;
#[cfg(test)]
mod tests;