Skip to main content

ifc_validate/type_check/
entity.rs

1//! Entity-level type checking: unknown types, abstract instantiation, values.
2
3use ifc_model::{Model, Value};
4use ifc_schema::Schema;
5
6use super::defined::{check, Mismatch};
7use super::select::admits_entity;
8use crate::report::{Finding, Path, Report};
9use crate::structure::expected_references;
10
11/// Reports entities whose type the schema does not declare.
12///
13/// A warning, not an error: the model is deliberately schema-agnostic and an
14/// unknown type round-trips intact. It is still worth saying, because it is
15/// usually a schema-version mismatch -- an IFC4X3 file read against IFC4.
16pub fn unknown_entity_types(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        if schema.entity(&entity.type_name).is_none() {
24            report.push(Finding::warning(
25                "type.entity.unknown",
26                Path::Entity(id),
27                format!(
28                    "{} is not declared by schema {}",
29                    entity.type_name,
30                    schema.name()
31                ),
32            ));
33        }
34    }
35}
36
37/// Reports instances of `ABSTRACT` entities.
38///
39/// EXPRESS `ABSTRACT SUPERTYPE` means the entity may not be instantiated
40/// directly; only its concrete subtypes may appear in a file.
41pub fn abstract_instances(model: &Model, schema: &Schema, report: &mut Report) {
42    let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
43    ids.sort_unstable();
44    for id in ids {
45        let Some(entity) = model.get(id) else {
46            continue;
47        };
48        let Some(definition) = schema.entity(&entity.type_name) else {
49            continue;
50        };
51        if definition.abstract_ {
52            report.push(Finding::error(
53                "type.entity.abstract",
54                Path::Entity(id),
55                format!("{} is abstract and cannot be instantiated", definition.name),
56            ));
57        }
58    }
59}
60
61/// Reports values that do not match their slot's declared type.
62///
63/// Aggregate members are checked against the element type, and every
64/// reference in a SELECT position against the SELECT's entity alternatives.
65pub fn attribute_types(model: &Model, schema: &Schema, report: &mut Report) {
66    let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
67    ids.sort_unstable();
68    for id in ids {
69        let Some(entity) = model.get(id) else {
70            continue;
71        };
72        let declared = schema.attributes(&entity.type_name);
73        for (index, value) in entity.attributes.iter().enumerate() {
74            let Some(attribute) = declared.get(index) else {
75                continue;
76            };
77            let path = || Path::Attribute {
78                entity: id,
79                index,
80                name: Some(attribute.name.clone()),
81            };
82            select_references(model, schema, &attribute.type_name, value, &path, report);
83            let Some(mismatch) = check(schema, &attribute.type_name, value) else {
84                continue;
85            };
86            let path = path();
87            let finding = match mismatch {
88                Mismatch::Primitive { expected, actual } => Finding::error(
89                    "type.scalar.mismatch",
90                    path,
91                    format!(
92                        "{} is {expected}, the file wrote {actual}",
93                        attribute.type_name
94                    ),
95                ),
96                Mismatch::FixedWidth { expected, actual } => Finding::error(
97                    "type.scalar.fixed_width",
98                    path,
99                    format!(
100                        "{} is STRING({expected}) FIXED, the file wrote {actual} characters",
101                        attribute.type_name
102                    ),
103                ),
104                Mismatch::EnumMember { member, declared } => Finding::error(
105                    "type.enumeration.member",
106                    path,
107                    format!(
108                        "{member} is not a member of {} ({})",
109                        attribute.type_name,
110                        if declared.is_empty() {
111                            "no members declared".to_string()
112                        } else {
113                            declared.join(", ")
114                        }
115                    ),
116                ),
117                Mismatch::SelectMember { written, select } => Finding::error(
118                    "type.select.member",
119                    path,
120                    format!("{written} is not a member of {select}"),
121                ),
122                Mismatch::ExpectedReference { declared, actual } => Finding::error(
123                    "type.entity.expected_reference",
124                    path,
125                    format!(
126                        "{} takes a reference to {declared}, the file wrote {actual}",
127                        attribute.name
128                    ),
129                ),
130            };
131            report.push(finding);
132        }
133    }
134}
135
136/// Reports references in SELECT positions whose target the SELECT does not
137/// admit.
138///
139/// A reference is admitted when its target is, or inherits from, an entity
140/// anywhere in the SELECT's closure. One finding per distinct offending
141/// target, in ascending order. A dangling target is `structure`'s finding,
142/// and a target of a type the schema does not declare is
143/// `type.entity.unknown`'s; neither is judged again here.
144fn select_references(
145    model: &Model,
146    schema: &Schema,
147    declared: &str,
148    value: &Value,
149    path: &dyn Fn() -> Path,
150    report: &mut Report,
151) {
152    let mut offenders = Vec::new();
153    for (expected, target) in expected_references(schema, declared, value) {
154        let Some(target_entity) = model.get(target) else {
155            continue;
156        };
157        if schema.entity(&target_entity.type_name).is_none() {
158            continue;
159        }
160        if admits_entity(schema, &expected, &target_entity.type_name) == Some(false) {
161            offenders.push((target, expected, &*target_entity.type_name));
162        }
163    }
164    offenders.sort_unstable();
165    offenders.dedup();
166    for (target, select, actual) in offenders {
167        report.push(Finding::error(
168            "type.select.member",
169            path(),
170            format!("{target} is {actual}, which is not a member of {select}"),
171        ));
172    }
173}