Skip to main content

ifc_validate/structure/
cardinality.rs

1//! Aggregates where the schema expects a scalar, and the reverse.
2//!
3//! What is caught here is the shape mismatch that actually occurs in the
4//! wild: a scalar written where an aggregate belongs, or an aggregate
5//! written where a scalar belongs. Both are usually a writer that guessed
6//! the slot layout. Bounds, inner nesting levels and element uniqueness are
7//! `bounds`'s findings.
8
9use ifc_model::{Model, Value};
10use ifc_schema::Schema;
11
12use crate::report::{Finding, Path, Report};
13use crate::type_check::aliased_aggregate;
14
15/// Reports scalar/aggregate shape mismatches against the schema.
16pub fn aggregate_shape(model: &Model, schema: &Schema, report: &mut Report) {
17    let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
18    ids.sort_unstable();
19    for id in ids {
20        let Some(entity) = model.get(id) else {
21            continue;
22        };
23        let declared = schema.attributes(&entity.type_name);
24        for (index, value) in entity.attributes.iter().enumerate() {
25            let Some(attribute) = declared.get(index) else {
26                continue;
27            };
28            // `$` and `*` carry no shape; optionality and derivation are
29            // checked in `required`.
30            if matches!(value, Value::Null | Value::Derived) {
31                continue;
32            }
33            let is_list = matches!(value.unwrap_typed(), Value::List(_));
34            // A defined type can itself be an aggregate: IFC4 declares
35            // `IfcComplexNumber = ARRAY [1:2] OF REAL` and four others. Such
36            // an attribute is declared scalar yet legitimately holds a list,
37            // so the declared type has to be resolved before judging shape.
38            // A typed wrapper names its own type, which overrides the
39            // declared slot type: `NominalValue : IfcValue` is a SELECT, and
40            // only `IFCCOMPLEXNUMBER((0.,0.))` says which member was chosen.
41            let effective_type = match value {
42                Value::Typed { type_name, .. } => type_name.as_ref(),
43                _ => attribute.type_name.as_str(),
44            };
45            let declared_is_aggregate =
46                attribute.aggregate || aliased_aggregate(schema, effective_type).is_some();
47            let path = || Path::Attribute {
48                entity: id,
49                index,
50                name: Some(attribute.name.clone()),
51            };
52            if declared_is_aggregate && !is_list {
53                report.push(Finding::error(
54                    "structure.cardinality.expected_aggregate",
55                    path(),
56                    format!("{} is an aggregate, a scalar was written", attribute.name),
57                ));
58            } else if !declared_is_aggregate && is_list {
59                report.push(Finding::error(
60                    "structure.cardinality.unexpected_aggregate",
61                    path(),
62                    format!("{} is a scalar, an aggregate was written", attribute.name),
63                ));
64            }
65        }
66    }
67}