use ifc_model::{Entity, EntityId, Model, Transaction, Value};
use ifc_schema::{for_version, Attribute, Schema, SchemaVersion, TypeKind};
use super::validate::projected_type;
use super::{CostAuthoringError, CostAuthoringResult};
#[derive(Debug, Clone, Copy)]
pub(super) struct Release {
version: SchemaVersion,
schema: &'static Schema,
}
const fn proven(version: SchemaVersion) -> bool {
matches!(
version,
SchemaVersion::Ifc2x3 | SchemaVersion::Ifc4 | SchemaVersion::Ifc4x3
)
}
pub(super) fn bind(model: &Model) -> CostAuthoringResult<Release> {
let version = match model.header().schema.as_slice() {
[] => SchemaVersion::Ifc4,
[token] => SchemaVersion::from_header_token(token)
.filter(|version| proven(*version))
.ok_or_else(|| CostAuthoringError::UnsupportedSchema {
schema: token.clone(),
})?,
tokens => {
return Err(CostAuthoringError::MultipleSchemas {
schemas: tokens.len(),
})
}
};
let schema = for_version(version).map_err(|_| CostAuthoringError::UnsupportedSchema {
schema: format!("{version:?}"),
})?;
Ok(Release { version, schema })
}
impl Release {
pub(super) const fn schema(self) -> &'static Schema {
self.schema
}
pub(super) const fn version(self) -> SchemaVersion {
self.version
}
pub(super) fn accepts_record(
self,
entity: &'static str,
attribute: &'static str,
record: &str,
) -> CostAuthoringResult<bool> {
let (_, declaration) = self.declared(entity, attribute)?;
Ok(self.schema.accepts_type(&declaration.type_name, record))
}
pub(super) fn enumerator(
self,
entity: &'static str,
attribute: &'static str,
token: &str,
) -> CostAuthoringResult<Option<&'static str>> {
let (_, declaration) = self.declared(entity, attribute)?;
Ok(
match self
.schema
.type_def(&declaration.type_name)
.map(|t| &t.kind)
{
Some(TypeKind::Enumeration(members)) => members
.iter()
.find(|member| member.trim().eq_ignore_ascii_case(token.trim()))
.map(String::as_str),
_ => None,
},
)
}
pub(super) fn require_entity(self, entity: &'static str) -> CostAuthoringResult<()> {
if self.schema.entity(entity).is_none_or(|e| e.abstract_) {
return Err(CostAuthoringError::EntityNotInSchema {
entity,
schema: self.version,
});
}
Ok(())
}
pub(super) fn declared(
self,
entity: &'static str,
attribute: &'static str,
) -> CostAuthoringResult<(usize, &'static Attribute)> {
self.require_entity(entity)?;
let name = release_name(self.version, entity, attribute);
self.schema
.attributes(entity)
.into_iter()
.enumerate()
.find(|(_, declared)| declared.name.eq_ignore_ascii_case(name))
.ok_or(CostAuthoringError::AuthoringNotInSchema {
entity,
attribute,
schema: self.version,
})
}
pub(super) fn record(
self,
entity: &'static str,
values: Vec<(&'static str, Value)>,
) -> CostAuthoringResult<Entity> {
self.require_entity(entity)?;
let declared = self.schema.attributes(entity);
let mut attributes = vec![Value::Null; declared.len()];
for (attribute, value) in values {
let (slot, declaration) = match self.declared(entity, attribute) {
Ok(found) => found,
Err(_) if value == Value::Null => continue,
Err(error) => return Err(error),
};
if !conforms(self.schema, declaration, &value) {
return Err(CostAuthoringError::AuthoringValueType {
entity,
attribute,
declared: declaration.type_name.as_str(),
schema: self.version,
});
}
attributes[slot] = value;
}
if let Some((declaration, _)) = declared
.iter()
.zip(&attributes)
.find(|(declaration, value)| !declaration.optional && **value == Value::Null)
{
return Err(CostAuthoringError::AuthoringRequired {
entity,
attribute: declaration.name.as_str(),
schema: self.version,
});
}
Ok(Entity::new(entity, attributes))
}
pub(super) fn require_owner_history(
self,
tx: &Transaction,
model: &Model,
entity: &'static str,
id: EntityId,
) -> CostAuthoringResult<()> {
let actual = projected_type(tx, model, id).ok_or(CostAuthoringError::MissingReference {
entity,
attribute: "OwnerHistory",
target: id,
})?;
let (_, declaration) = self.declared(entity, "OwnerHistory")?;
if self.schema.accepts_type(&declaration.type_name, &actual) {
return Ok(());
}
Err(CostAuthoringError::WrongReferenceType {
entity,
attribute: "OwnerHistory",
target: id,
actual,
expected: "IFCOWNERHISTORY",
})
}
}
fn release_name(release: SchemaVersion, entity: &str, attribute: &'static str) -> &'static str {
match (release, entity, attribute) {
(SchemaVersion::Ifc2x3, "IFCCOSTSCHEDULE", "Identification") => "ID",
(SchemaVersion::Ifc2x3, "IFCCOSTVALUE", "Category") => "CostType",
_ => attribute,
}
}
fn conforms(schema: &Schema, declared: &Attribute, value: &Value) -> bool {
match value {
Value::Null => true,
Value::List(items) => {
declared.aggregate
&& items
.iter()
.all(|item| scalar(schema, &declared.type_name, item))
}
_ => !declared.aggregate && scalar(schema, &declared.type_name, value),
}
}
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::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,
}
}
#[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(CostAuthoringError::UnsupportedSchema { schema }) if schema == token),
"{token} must be refused"
);
}
}
}