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;
4use ifc_schema::Schema;
5
6use super::defined::{check, Mismatch};
7use crate::report::{Finding, Path, Report};
8
9/// Reports entities whose type the schema does not declare.
10///
11/// A warning, not an error: the model is deliberately schema-agnostic and an
12/// unknown type round-trips intact. It is still worth saying, because it is
13/// usually a schema-version mismatch -- an IFC4X3 file read against IFC4.
14pub fn unknown_entity_types(model: &Model, schema: &Schema, report: &mut Report) {
15    let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
16    ids.sort_unstable();
17    for id in ids {
18        let Some(entity) = model.get(id) else {
19            continue;
20        };
21        if schema.entity(&entity.type_name).is_none() {
22            report.push(Finding::warning(
23                "type.entity.unknown",
24                Path::Entity(id),
25                format!(
26                    "{} is not declared by schema {}",
27                    entity.type_name,
28                    schema.name()
29                ),
30            ));
31        }
32    }
33}
34
35/// Reports instances of `ABSTRACT` entities.
36///
37/// EXPRESS `ABSTRACT SUPERTYPE` means the entity may not be instantiated
38/// directly; only its concrete subtypes may appear in a file.
39pub fn abstract_instances(model: &Model, schema: &Schema, report: &mut Report) {
40    let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
41    ids.sort_unstable();
42    for id in ids {
43        let Some(entity) = model.get(id) else {
44            continue;
45        };
46        let Some(definition) = schema.entity(&entity.type_name) else {
47            continue;
48        };
49        if definition.abstract_ {
50            report.push(Finding::error(
51                "type.entity.abstract",
52                Path::Entity(id),
53                format!("{} is abstract and cannot be instantiated", definition.name),
54            ));
55        }
56    }
57}
58
59/// Reports values that do not match their slot's declared type.
60pub fn attribute_types(model: &Model, schema: &Schema, report: &mut Report) {
61    let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
62    ids.sort_unstable();
63    for id in ids {
64        let Some(entity) = model.get(id) else {
65            continue;
66        };
67        let declared = schema.attributes(&entity.type_name);
68        for (index, value) in entity.attributes.iter().enumerate() {
69            let Some(attribute) = declared.get(index) else {
70                continue;
71            };
72            let Some(mismatch) = check(schema, &attribute.type_name, value) else {
73                continue;
74            };
75            let path = Path::Attribute {
76                entity: id,
77                index,
78                name: Some(attribute.name.clone()),
79            };
80            let finding = match mismatch {
81                Mismatch::Primitive { expected, actual } => Finding::error(
82                    "type.scalar.mismatch",
83                    path,
84                    format!(
85                        "{} is {expected}, the file wrote {actual}",
86                        attribute.type_name
87                    ),
88                ),
89                Mismatch::FixedWidth { expected, actual } => Finding::error(
90                    "type.scalar.fixed_width",
91                    path,
92                    format!(
93                        "{} is STRING({expected}) FIXED, the file wrote {actual} characters",
94                        attribute.type_name
95                    ),
96                ),
97                Mismatch::EnumMember { member, declared } => Finding::error(
98                    "type.enumeration.member",
99                    path,
100                    format!(
101                        "{member} is not a member of {} ({})",
102                        attribute.type_name,
103                        if declared.is_empty() {
104                            "no members declared".to_string()
105                        } else {
106                            declared.join(", ")
107                        }
108                    ),
109                ),
110                Mismatch::SelectMember { written, select } => Finding::error(
111                    "type.select.member",
112                    path,
113                    format!("{written} is not a member of {select}"),
114                ),
115            };
116            report.push(finding);
117        }
118    }
119}