use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::{for_version, Schema, SchemaVersion};
use crate::error::{PropertyError, PropertyResult};
use crate::quantity::set::QuantityKind;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Layout {
version: SchemaVersion,
schema: &'static Schema,
}
pub(crate) fn layout(version: SchemaVersion) -> PropertyResult<Layout> {
let schema = for_version(version).ok_or_else(|| PropertyError::UnsupportedSchema {
schema: format!("{version:?}"),
})?;
Ok(Layout { version, schema })
}
pub(crate) fn bind(model: &Model) -> PropertyResult<Layout> {
let version = match model.header().schema.as_slice() {
[] => SchemaVersion::Ifc4,
[token] => SchemaVersion::from_header_token(token).ok_or_else(|| {
PropertyError::UnsupportedSchema {
schema: token.clone(),
}
})?,
tokens => {
return Err(PropertyError::MultipleSchemas {
schemas: tokens.len(),
})
}
};
layout(version)
}
impl Layout {
pub(crate) fn version(self) -> SchemaVersion {
self.version
}
pub(crate) fn schema(self) -> &'static Schema {
self.schema
}
pub(super) fn entity(self, kind: QuantityKind) -> PropertyResult<String> {
let entity = kind.type_name().to_ascii_uppercase();
if self.schema.entity(&entity).is_some_and(|e| !e.abstract_) {
Ok(entity)
} else {
Err(PropertyError::EntityNotInSchema {
entity: kind.type_name(),
schema: self.version,
})
}
}
pub(super) fn arity(self, entity: &str) -> usize {
self.schema.attributes(entity).len()
}
pub(super) fn slot(self, entity: &str, attribute: &str) -> Option<usize> {
self.schema
.attribute_names(entity)
.iter()
.position(|name| name.eq_ignore_ascii_case(attribute))
}
pub(super) fn scalar(self, kind: QuantityKind, value: f64) -> PropertyResult<Value> {
let entity = kind.type_name();
let attribute = kind.value_attribute();
if !value.is_finite() {
return Err(PropertyError::AuthoringInvalid {
entity,
attribute,
value: value.to_string(),
});
}
if kind != QuantityKind::Count {
return Ok(Value::Real(value));
}
if value.fract() == 0.0 {
#[allow(clippy::cast_possible_truncation)]
return Ok(Value::Integer(value as i64));
}
if self.schema.resolve_defined("IFCCOUNTMEASURE") == "INTEGER" {
return Err(PropertyError::AuthoringInvalid {
entity,
attribute,
value: format!("{value} (IfcCountMeasure is INTEGER in {:?})", self.version),
});
}
Ok(Value::Real(value))
}
pub(super) fn record(
self,
kind: QuantityKind,
entity: &str,
values: Vec<(&'static str, Value)>,
) -> PropertyResult<Entity> {
let mut attributes = vec![Value::Null; self.arity(entity)];
for (attribute, value) in values {
match self.slot(entity, attribute) {
Some(slot) => attributes[slot] = value,
None if value == Value::Null => {}
None => {
return Err(PropertyError::AuthoringNotInSchema {
entity: kind.type_name(),
attribute,
schema: self.version,
})
}
}
}
Ok(Entity::new(kind.type_name(), attributes))
}
pub(crate) fn named_record(
self,
entity: &'static str,
values: Vec<(&'static str, Value)>,
) -> PropertyResult<Entity> {
if self.schema.entity(entity).is_none_or(|e| e.abstract_) {
return Err(PropertyError::EntityNotInSchema {
entity,
schema: self.version,
});
}
let declared = self.schema.attributes(entity);
let mut attributes = vec![Value::Null; declared.len()];
for (attribute, value) in values {
match self.slot(entity, attribute) {
Some(slot) => attributes[slot] = value,
None if value == Value::Null => {}
None => {
return Err(PropertyError::AuthoringNotInSchema {
entity,
attribute,
schema: self.version,
})
}
}
}
for (declaration, value) in declared.iter().zip(&attributes) {
if *value == Value::Null && !declaration.optional {
return Err(PropertyError::AuthoringRequired {
entity,
attribute: declaration.name.as_str(),
schema: self.version,
});
}
}
Ok(Entity::new(entity, attributes))
}
}
pub(crate) fn projected(tx: &Transaction, model: &Model, id: EntityId) -> Option<Entity> {
let mut current = model.get(id).cloned();
for edit in tx.edits() {
match edit {
Edit::Create { id: target, entity } if *target == id => {
current = Some(entity.clone());
}
Edit::SetAttribute {
id: target,
slot,
value,
} if *target == id => {
if let Some(attribute) = current
.as_mut()
.and_then(|entity| entity.attributes.get_mut(*slot))
{
*attribute = value.clone();
}
}
Edit::Retype {
id: target,
type_name,
} if *target == id => {
if let Some(entity) = current.as_mut() {
entity.type_name = type_name.clone();
}
}
Edit::Remove { id: target } if *target == id => current = None,
_ => {}
}
}
current
}