1use ifc_model::{Model, Value};
4use ifc_schema::Schema;
5
6use super::defined::{check_all, Mismatch};
7use super::select::admits_entity;
8use crate::report::{Finding, Path, Report};
9use crate::structure::expected_references;
10
11pub 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
37pub 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
61pub 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 for mismatch in check_all(schema, &attribute.type_name, value) {
84 report.push(finding(
85 mismatch,
86 path(),
87 &attribute.type_name,
88 &attribute.name,
89 ));
90 }
91 }
92 }
93}
94
95fn finding(mismatch: Mismatch, path: Path, declared: &str, attribute: &str) -> Finding {
97 match mismatch {
98 Mismatch::Primitive { expected, actual } => Finding::error(
99 "type.scalar.mismatch",
100 path,
101 format!("{declared} is {expected}, the file wrote {actual}"),
102 ),
103 Mismatch::FixedWidth { expected, actual } => Finding::error(
104 "type.scalar.fixed_width",
105 path,
106 format!("{declared} is STRING({expected}) FIXED, the file wrote {actual} characters"),
107 ),
108 Mismatch::EnumMember {
109 member,
110 declared: members,
111 } => Finding::error(
112 "type.enumeration.member",
113 path,
114 format!(
115 "{member} is not a member of {declared} ({})",
116 if members.is_empty() {
117 "no members declared".to_string()
118 } else {
119 members.join(", ")
120 }
121 ),
122 ),
123 Mismatch::SelectMember { written, select } => Finding::error(
124 "type.select.member",
125 path,
126 format!("{written} is not a member of {select}"),
127 ),
128 Mismatch::ExpectedReference { declared, actual } => Finding::error(
129 "type.entity.expected_reference",
130 path,
131 format!("{attribute} takes a reference to {declared}, the file wrote {actual}"),
132 ),
133 Mismatch::TypedOutsideSelect { written, declared } => Finding::error(
134 "type.typed.outside_select",
135 path,
136 format!(
137 "{declared} is not a SELECT, so its value is written bare, \
138 not as the typed parameter {written}(...)"
139 ),
140 ),
141 Mismatch::TypedWrongType { written, declared } => Finding::error(
142 "type.typed.wrong_type",
143 path,
144 format!("{written} is not {declared}, and {declared} is not a SELECT"),
145 ),
146 Mismatch::UntypedSelectValue { select, actual } => Finding::error(
147 "type.select.untyped",
148 path,
149 format!(
150 "{select} is a SELECT, so a value that is not a reference is \
151 written as a typed parameter; the file wrote {actual}"
152 ),
153 ),
154 }
155}
156
157fn select_references(
166 model: &Model,
167 schema: &Schema,
168 declared: &str,
169 value: &Value,
170 path: &dyn Fn() -> Path,
171 report: &mut Report,
172) {
173 let mut offenders = Vec::new();
174 for (expected, target) in expected_references(schema, declared, value) {
175 let Some(target_entity) = model.get(target) else {
176 continue;
177 };
178 if schema.entity(&target_entity.type_name).is_none() {
179 continue;
180 }
181 if admits_entity(schema, &expected, &target_entity.type_name) == Some(false) {
182 offenders.push((target, expected, &*target_entity.type_name));
183 }
184 }
185 offenders.sort_unstable();
186 offenders.dedup();
187 for (target, select, actual) in offenders {
188 report.push(Finding::error(
189 "type.select.member",
190 path(),
191 format!("{target} is {actual}, which is not a member of {select}"),
192 ));
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use ifc_model::Entity;
200
201 #[test]
204 fn a_slot_reports_each_independent_violation() {
205 let schema = ifc_schema::ifc4();
206 let mut model = Model::new();
207 let names = schema.attribute_names("IFCCARTESIANPOINTLIST3D");
208 let mut attributes = vec![Value::Null; names.len()];
209 attributes[0] = Value::List(vec![
210 Value::List(vec![
211 Value::Text("x".into()),
212 Value::Real(0.0),
213 Value::Real(0.0),
214 ]),
215 Value::List(vec![
216 Value::Real(0.0),
217 Value::Typed {
218 type_name: "IFCLENGTHMEASURE".into(),
219 value: Box::new(Value::Real(1.0)),
220 },
221 Value::Real(0.0),
222 ]),
223 ]);
224 let id = model.push(Entity::new("IFCCARTESIANPOINTLIST3D", attributes));
225 let mut report = Report::new();
226 attribute_types(&model, schema, &mut report);
227 let rules: Vec<&str> = report
228 .findings()
229 .iter()
230 .filter(|finding| matches!(finding.path, Path::Attribute { entity, index: 0, .. } if entity == id))
231 .map(|finding| finding.rule.as_str())
232 .collect();
233 assert_eq!(rules, ["type.scalar.mismatch", "type.typed.outside_select"]);
234 }
235}