use ifc_model::{EntityId, Model, Value};
use ifc_schema::Schema;
use crate::report::{Finding, Path, Report};
use crate::type_check::element_type;
pub fn dangling_references(model: &Model, report: &mut Report) {
let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
ids.sort_unstable();
for id in ids {
let Some(entity) = model.get(id) else {
continue;
};
for (index, value) in entity.attributes.iter().enumerate() {
let mut missing = Vec::new();
value.for_each_ref(&mut |target| {
if model.get(target).is_none() {
missing.push(target);
}
});
missing.sort_unstable();
missing.dedup();
for target in missing {
report.push(Finding::error(
"structure.reference.dangling",
Path::Attribute {
entity: id,
index,
name: None,
},
format!("references {target}, which the file does not contain"),
));
}
}
}
}
pub fn wrong_kind_references(model: &Model, schema: &Schema, report: &mut Report) {
let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
ids.sort_unstable();
for id in ids {
let Some(entity) = model.get(id) else {
continue;
};
let declared = schema.attributes(&entity.type_name);
for (index, value) in entity.attributes.iter().enumerate() {
let Some(attribute) = declared.get(index) else {
continue;
};
let mut offenders: Vec<(EntityId, String, &str)> = Vec::new();
for (expected, target) in expected_references(schema, &attribute.type_name, value) {
if schema.entity(&expected).is_none() {
continue;
}
let Some(target_entity) = model.get(target) else {
continue; };
if schema.entity(&target_entity.type_name).is_none() {
continue;
}
if !schema.is_a(&target_entity.type_name, &expected) {
offenders.push((target, expected, &target_entity.type_name));
}
}
offenders.sort_unstable();
offenders.dedup();
for (target, expected, actual) in offenders {
report.push(Finding::error(
"structure.reference.wrong_type",
Path::Attribute {
entity: id,
index,
name: Some(attribute.name.clone()),
},
format!("declared {expected} but {target} is {actual}"),
));
}
}
}
}
const MAX_NESTING: usize = 8;
pub(crate) fn expected_references(
schema: &Schema,
declared: &str,
value: &Value,
) -> Vec<(String, EntityId)> {
let mut found = Vec::new();
collect(schema, declared, value, 0, &mut found);
found
}
fn collect(
schema: &Schema,
declared: &str,
value: &Value,
depth: usize,
found: &mut Vec<(String, EntityId)>,
) {
match value {
Value::Ref(target) => found.push((declared.to_string(), *target)),
Value::List(items) if depth < MAX_NESTING => {
let element = element_type(schema, declared);
for item in items {
collect(schema, &element, item, depth + 1, found);
}
}
Value::Typed { type_name, value } => collect(schema, type_name, value, depth, found),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_undeclared_target_type_is_not_judged() {
let schema = ifc_schema::ifc4();
let mut model = Model::new();
model.insert(
EntityId(1),
ifc_model::Entity::new("IFCREFERENT", Vec::new()),
);
model.insert(
EntityId(2),
ifc_model::Entity::new("IFCPROPERTYSET", Vec::new()),
);
let mut attributes =
vec![Value::Null; schema.attributes("IFCRELDEFINESBYPROPERTIES").len()];
attributes[4] = Value::List(vec![Value::Ref(EntityId(1)), Value::Ref(EntityId(2))]);
model.insert(
EntityId(3),
ifc_model::Entity::new("IFCRELDEFINESBYPROPERTIES", attributes),
);
let mut report = Report::new();
wrong_kind_references(&model, schema, &mut report);
let messages: Vec<&str> = report
.findings()
.iter()
.map(|finding| finding.message.as_str())
.collect();
assert_eq!(
messages,
["declared IfcObjectDefinition but #2 is IFCPROPERTYSET"]
);
}
#[test]
fn references_are_paired_with_the_type_their_position_expects() {
let schema = ifc_schema::ifc4();
let set = Value::List(vec![Value::Ref(EntityId(1)), Value::Ref(EntityId(2))]);
assert_eq!(
expected_references(schema, "IfcProduct", &set),
[
("IfcProduct".to_string(), EntityId(1)),
("IfcProduct".to_string(), EntityId(2))
]
);
let typed = Value::Typed {
type_name: "IFCPROPERTYSETDEFINITIONSET".into(),
value: Box::new(Value::List(vec![Value::Ref(EntityId(3))])),
};
assert_eq!(
expected_references(schema, "IfcPropertySetDefinitionSelect", &typed),
[("IfcPropertySetDefinition".to_string(), EntityId(3))]
);
}
}