use ifc_model::{Model, Value};
use ifc_schema::{Schema, TypeKind};
use crate::report::{Finding, Path, Report};
pub fn aggregate_shape(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;
};
if matches!(value, Value::Null | Value::Derived) {
continue;
}
let is_list = matches!(value.unwrap_typed(), Value::List(_));
let effective_type = match value {
Value::Typed { type_name, .. } => type_name.as_ref(),
_ => attribute.type_name.as_str(),
};
let declared_is_aggregate =
attribute.aggregate || type_is_aggregate(schema, effective_type);
let path = || Path::Attribute {
entity: id,
index,
name: Some(attribute.name.clone()),
};
if declared_is_aggregate && !is_list {
report.push(Finding::error(
"structure.cardinality.expected_aggregate",
path(),
format!("{} is an aggregate, a scalar was written", attribute.name),
));
} else if !declared_is_aggregate && is_list {
report.push(Finding::error(
"structure.cardinality.unexpected_aggregate",
path(),
format!("{} is a scalar, an aggregate was written", attribute.name),
));
}
}
}
}
fn type_is_aggregate(schema: &Schema, type_name: &str) -> bool {
let mut current = type_name.to_string();
for _ in 0..16 {
let Some(definition) = schema.type_def(¤t) else {
return false;
};
let TypeKind::Defined(target) = &definition.kind else {
return false;
};
let head = target.trim_start().to_ascii_uppercase();
if head.starts_with("LIST")
|| head.starts_with("ARRAY")
|| head.starts_with("SET")
|| head.starts_with("BAG")
{
return true;
}
current = target.trim().to_string();
}
false
}