use ifc_model::{Model, Value};
use ifc_schema::Schema;
use super::defined::{check_all, Mismatch};
use super::select::admits_entity;
use crate::report::{Finding, Path, Report};
use crate::structure::expected_references;
pub fn unknown_entity_types(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;
};
if schema.entity(&entity.type_name).is_none() {
report.push(Finding::warning(
"type.entity.unknown",
Path::Entity(id),
format!(
"{} is not declared by schema {}",
entity.type_name,
schema.name()
),
));
}
}
}
pub fn abstract_instances(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 Some(definition) = schema.entity(&entity.type_name) else {
continue;
};
if definition.abstract_ {
report.push(Finding::error(
"type.entity.abstract",
Path::Entity(id),
format!("{} is abstract and cannot be instantiated", definition.name),
));
}
}
}
pub fn attribute_types(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 path = || Path::Attribute {
entity: id,
index,
name: Some(attribute.name.clone()),
};
select_references(model, schema, &attribute.type_name, value, &path, report);
for mismatch in check_all(schema, &attribute.type_name, value) {
report.push(finding(
mismatch,
path(),
&attribute.type_name,
&attribute.name,
));
}
}
}
}
fn finding(mismatch: Mismatch, path: Path, declared: &str, attribute: &str) -> Finding {
match mismatch {
Mismatch::Primitive { expected, actual } => Finding::error(
"type.scalar.mismatch",
path,
format!("{declared} is {expected}, the file wrote {actual}"),
),
Mismatch::FixedWidth { expected, actual } => Finding::error(
"type.scalar.fixed_width",
path,
format!("{declared} is STRING({expected}) FIXED, the file wrote {actual} characters"),
),
Mismatch::EnumMember {
member,
declared: members,
} => Finding::error(
"type.enumeration.member",
path,
format!(
"{member} is not a member of {declared} ({})",
if members.is_empty() {
"no members declared".to_string()
} else {
members.join(", ")
}
),
),
Mismatch::SelectMember { written, select } => Finding::error(
"type.select.member",
path,
format!("{written} is not a member of {select}"),
),
Mismatch::ExpectedReference { declared, actual } => Finding::error(
"type.entity.expected_reference",
path,
format!("{attribute} takes a reference to {declared}, the file wrote {actual}"),
),
Mismatch::TypedOutsideSelect { written, declared } => Finding::error(
"type.typed.outside_select",
path,
format!(
"{declared} is not a SELECT, so its value is written bare, \
not as the typed parameter {written}(...)"
),
),
Mismatch::TypedWrongType { written, declared } => Finding::error(
"type.typed.wrong_type",
path,
format!("{written} is not {declared}, and {declared} is not a SELECT"),
),
Mismatch::UntypedSelectValue { select, actual } => Finding::error(
"type.select.untyped",
path,
format!(
"{select} is a SELECT, so a value that is not a reference is \
written as a typed parameter; the file wrote {actual}"
),
),
}
}
fn select_references(
model: &Model,
schema: &Schema,
declared: &str,
value: &Value,
path: &dyn Fn() -> Path,
report: &mut Report,
) {
let mut offenders = Vec::new();
for (expected, target) in expected_references(schema, declared, value) {
let Some(target_entity) = model.get(target) else {
continue;
};
if schema.entity(&target_entity.type_name).is_none() {
continue;
}
if admits_entity(schema, &expected, &target_entity.type_name) == Some(false) {
offenders.push((target, expected, &*target_entity.type_name));
}
}
offenders.sort_unstable();
offenders.dedup();
for (target, select, actual) in offenders {
report.push(Finding::error(
"type.select.member",
path(),
format!("{target} is {actual}, which is not a member of {select}"),
));
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Entity;
#[test]
fn a_slot_reports_each_independent_violation() {
let schema = ifc_schema::ifc4();
let mut model = Model::new();
let names = schema.attribute_names("IFCCARTESIANPOINTLIST3D");
let mut attributes = vec![Value::Null; names.len()];
attributes[0] = Value::List(vec![
Value::List(vec![
Value::Text("x".into()),
Value::Real(0.0),
Value::Real(0.0),
]),
Value::List(vec![
Value::Real(0.0),
Value::Typed {
type_name: "IFCLENGTHMEASURE".into(),
value: Box::new(Value::Real(1.0)),
},
Value::Real(0.0),
]),
]);
let id = model.push(Entity::new("IFCCARTESIANPOINTLIST3D", attributes));
let mut report = Report::new();
attribute_types(&model, schema, &mut report);
let rules: Vec<&str> = report
.findings()
.iter()
.filter(|finding| matches!(finding.path, Path::Attribute { entity, index: 0, .. } if entity == id))
.map(|finding| finding.rule.as_str())
.collect();
assert_eq!(rules, ["type.scalar.mismatch", "type.typed.outside_select"]);
}
}