use std::{collections::BTreeSet, sync::Arc};
use ifc_model::{Entity, EntityId, Model, Value};
use ifc_schema::TypeKind;
use super::assignment::assigned_sets;
use super::composite::{entity_target, enum_accepts};
use super::refs::text_at;
use super::release::{validate_model, Release};
use super::set::{load_named, Member, SetKind};
use super::value::{
exact_value, select_member, simple_payload_matches, typed_payload_matches, ResolvedValue,
};
use super::{
ExactEntityRef, ExactLogical, ExactProperty, ExactPropertyEntry, ExactPropertyError,
ExactSource, ExactValue,
};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ExactPredefinedSet {
pub source: ExactSource,
pub set_id: EntityId,
pub entity: Arc<str>,
pub name: Option<Arc<str>>,
pub attributes: Vec<ExactPropertyEntry>,
}
impl ExactPredefinedSet {
#[must_use]
pub fn attribute(&self, name: &str) -> Option<&ExactProperty> {
self.attributes
.iter()
.find(|entry| entry.name.as_ref() == name)
.map(|entry| &entry.property)
}
}
pub fn exact_predefined_sets(
model: &Model,
object: EntityId,
entity: &str,
) -> Result<Vec<ExactPredefinedSet>, ExactPropertyError> {
let release = validate_model(model)?;
let schema = release.schema;
let predefined = schema.entity(entity).is_some()
&& schema.is_a(entity, "IFCPROPERTYSETDEFINITION")
&& !schema.is_a("IFCPROPERTYSET", entity)
&& !schema.is_a("IFCELEMENTQUANTITY", entity);
if !predefined {
return Err(ExactPropertyError::NotAPredefinedSet {
name: entity.into(),
schema: release.version,
});
}
let assigned = assigned_sets(model, release, object)?;
let mut sources = vec![(ExactSource::Occurrence, &assigned.occurrence_sets)];
if let Some((type_id, sets)) = &assigned.type_sets {
sources.push((ExactSource::Type(*type_id), sets));
}
let mut found = Vec::new();
for (source, sets) in sources {
for &set_id in sets {
let set = load_named(model, release, set_id)?;
if set.kind != SetKind::Predefined || !schema.is_a(&set.entity.type_name, entity) {
continue;
}
let attributes = own_attributes(release, set.entity)
.map(|(slot, name)| {
let member = Member::Attribute(slot);
let resolved = set.value(model, release, member)?;
Ok(ExactPropertyEntry {
name: name.into(),
property: set.exact(source, member, resolved),
})
})
.collect::<Result<_, ExactPropertyError>>()?;
found.push(ExactPredefinedSet {
source,
set_id,
entity: canonical_name(release, set.entity).into(),
name: set.named.then(|| set.name.into()),
attributes,
});
}
}
Ok(found)
}
pub(super) fn predefined_key(
release: Release,
set_id: EntityId,
set: &Entity,
) -> Result<(&str, bool), ExactPropertyError> {
let Some((slot, attribute)) = release.attribute(&set.type_name, "Name") else {
return Ok((canonical_name(release, set), false));
};
match set.attributes.get(slot) {
Some(Value::Null) if attribute.optional => Ok((canonical_name(release, set), false)),
name => text_at(set_id, name, "Name").map(|name| (name, true)),
}
}
fn canonical_name(release: Release, set: &Entity) -> &'static str {
release
.schema
.entity(&set.type_name)
.map_or("", |definition| definition.name.as_str())
}
pub(super) fn own_attributes(
release: Release,
set: &Entity,
) -> impl Iterator<Item = (usize, &'static str)> {
let schema = release.schema;
let base = if schema.is_a(set.type_name.as_ref(), "IFCMATERIALPROPERTIES") {
"IFCMATERIALPROPERTIES"
} else {
"IFCPROPERTYSETDEFINITION"
};
let inherited = schema.attribute_names(base).len();
schema
.attributes(set.type_name.as_ref())
.into_iter()
.enumerate()
.skip(inherited)
.map(|(slot, attribute)| (slot, attribute.name.as_str()))
}
pub(super) fn attribute_value(
model: &Model,
release: Release,
set_id: EntityId,
set: &Entity,
slot: usize,
) -> Result<ResolvedValue, ExactPropertyError> {
let schema = release.schema;
let attribute = schema.attributes(&set.type_name)[slot];
if attribute.aggregate {
return Err(ExactPropertyError::UnsupportedDefinition {
entity: set_id,
type_name: set.type_name.clone(),
});
}
let declared: Arc<str> = attribute.type_name.to_ascii_uppercase().into();
let value = &set.attributes[slot];
let scalar = |value, value_type| {
Ok(ResolvedValue {
value,
value_type: Some(value_type),
unit_id: None,
})
};
let unsupported = || ExactPropertyError::UnsupportedValue { property: set_id };
match value {
Value::Null if attribute.optional => return scalar(ExactValue::Null, declared),
Value::Null => return Err(ExactPropertyError::MissingValueSlot { property: set_id }),
_ => {}
}
let entity_ref = |target: EntityId| {
let entity = entity_target(model, release, set_id, target, attribute)?;
Ok(ExactValue::Entity(ExactEntityRef {
id: target,
type_name: entity.type_name.clone(),
}))
};
if schema.entity(&declared).is_some() {
let Value::Ref(target) = value else {
return Err(unsupported());
};
return scalar(entity_ref(*target)?, declared);
}
match schema
.type_def(&declared)
.map(|definition| &definition.kind)
{
Some(TypeKind::Enumeration(_)) => match value {
Value::Enum(member) if enum_accepts(release, &declared, member) => {
scalar(ExactValue::Enum(member.clone()), declared)
}
_ => Err(unsupported()),
},
Some(TypeKind::Select(_)) => match value {
Value::Ref(target) => scalar(entity_ref(*target)?, declared),
value => {
let typed = select_member(release, set_id, &declared, value)?;
scalar(typed.value, typed.value_type)
}
},
Some(TypeKind::Defined(_))
if typed_payload_matches(schema, &declared, value, &mut BTreeSet::new()) =>
{
let logical = schema
.resolve_defined(&declared)
.eq_ignore_ascii_case("LOGICAL");
scalar(bare(set_id, value, logical)?, declared)
}
None if simple_payload_matches(&declared, value) => {
scalar(bare(set_id, value, &*declared == "LOGICAL")?, declared)
}
_ => Err(unsupported()),
}
}
fn bare(set_id: EntityId, value: &Value, logical: bool) -> Result<ExactValue, ExactPropertyError> {
Ok(match (exact_value(set_id, Some(value))?, logical) {
(ExactValue::Bool(true), true) => ExactValue::Logical(ExactLogical::True),
(ExactValue::Bool(false), true) => ExactValue::Logical(ExactLogical::False),
(other, _) => other,
})
}
#[cfg(test)]
mod tests {
use ifc_model::{Entity, EntityId, Model, Value};
use ifc_schema::{Schema, SchemaVersion};
use super::{attribute_value, Release};
use crate::{ExactLogical, ExactValue};
const EXPRESS: &str = "SCHEMA IFC4;
TYPE IfcLogical = LOGICAL;
END_TYPE;
ENTITY IfcPropertySetDefinition;
GlobalId : STRING;
OwnerHistory : OPTIONAL STRING;
Name : OPTIONAL STRING;
Description : OPTIONAL STRING;
END_ENTITY;
ENTITY IfcFlagProperties
SUBTYPE OF (IfcPropertySetDefinition);
Flag : OPTIONAL IfcLogical;
Bare : OPTIONAL LOGICAL;
END_ENTITY;
END_SCHEMA;";
#[test]
fn a_logical_attribute_keeps_three_states() {
let release = Release {
version: SchemaVersion::Ifc4,
schema: Box::leak(Box::new(Schema::from_express(EXPRESS))),
};
let model = Model::new();
for (value, expected) in [
(Value::Bool(true), ExactLogical::True),
(Value::Bool(false), ExactLogical::False),
(Value::LogicalUnknown, ExactLogical::Unknown),
] {
for slot in [4, 5] {
let mut attributes = vec![Value::Null; 6];
attributes[slot] = value.clone();
let set = Entity::new("IFCFLAGPROPERTIES", attributes);
let resolved = attribute_value(&model, release, EntityId(1), &set, slot)
.expect("a logical resolves");
assert_eq!(resolved.value, ExactValue::Logical(expected), "slot {slot}");
}
}
}
}