use ifc_model::{Model, Value};
use ifc_schema::Schema;
use super::defined::{check, 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);
let Some(mismatch) = check(schema, &attribute.type_name, value) else {
continue;
};
let path = path();
let finding = match mismatch {
Mismatch::Primitive { expected, actual } => Finding::error(
"type.scalar.mismatch",
path,
format!(
"{} is {expected}, the file wrote {actual}",
attribute.type_name
),
),
Mismatch::FixedWidth { expected, actual } => Finding::error(
"type.scalar.fixed_width",
path,
format!(
"{} is STRING({expected}) FIXED, the file wrote {actual} characters",
attribute.type_name
),
),
Mismatch::EnumMember { member, declared } => Finding::error(
"type.enumeration.member",
path,
format!(
"{member} is not a member of {} ({})",
attribute.type_name,
if declared.is_empty() {
"no members declared".to_string()
} else {
declared.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!(
"{} takes a reference to {declared}, the file wrote {actual}",
attribute.name
),
),
};
report.push(finding);
}
}
}
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}"),
));
}
}