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}