ifc_validate/type_check/defined.rs
1//! Values checked against the defined type declared for their slot.
2//!
3//! Both what a value is and the form it is written in are judged: Part 21
4//! writes a typed parameter exactly where the declared type is a SELECT, and
5//! a bare value everywhere else (see `typed`).
6
7use ifc_model::Value;
8use ifc_schema::Schema;
9
10use super::aggregate::element_type;
11use super::enumeration;
12use super::scalar::{describe_value, primitive_of, FixedWidth};
13use super::select;
14use super::typed::wrapper_fits;
15
16/// How deeply nested aggregates are descended into.
17///
18/// The deepest aggregate IFC declares is three levels (a list of lists of
19/// measures); anything deeper is left unjudged rather than walked without
20/// bound, since the nesting depth is under the file's control.
21const MAX_NESTING: usize = 8;
22
23/// Why a value does not fit its declared type.
24#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum Mismatch {
27 /// The written form is not what the primitive accepts.
28 Primitive {
29 /// What the schema expects, in words.
30 expected: &'static str,
31 /// What the file wrote, in words.
32 actual: &'static str,
33 },
34 /// An enumeration constant the schema does not declare.
35 EnumMember {
36 /// The constant as written.
37 member: String,
38 /// The members the schema declares.
39 declared: Vec<String>,
40 },
41 /// A typed value whose type is not a member of the declared SELECT.
42 SelectMember {
43 /// The wrapper type as written.
44 written: String,
45 /// The SELECT it had to belong to.
46 select: String,
47 },
48 /// A `STRING(n) FIXED` value written at the wrong width.
49 FixedWidth {
50 /// The width the schema fixes.
51 expected: usize,
52 /// The width actually written.
53 actual: usize,
54 },
55 /// A value that is not an entity reference, in a slot only an entity
56 /// reference can fill: an entity-typed slot, or a SELECT of entities.
57 ExpectedReference {
58 /// The entity or SELECT the slot declares.
59 declared: String,
60 /// What the file wrote, in words.
61 actual: &'static str,
62 },
63 /// A typed parameter of the declared type, or of a specialisation of
64 /// it, where the declared type is not a SELECT. ISO 10303-21:2016
65 /// §12.1.6 writes such a value bare.
66 TypedOutsideSelect {
67 /// The wrapper type as written.
68 written: String,
69 /// The non-SELECT type the slot declares.
70 declared: String,
71 },
72 /// A typed parameter whose type is not the declared non-SELECT type:
73 /// the wrong form and the wrong type.
74 TypedWrongType {
75 /// The wrapper type as written.
76 written: String,
77 /// The non-SELECT type the slot declares.
78 declared: String,
79 },
80 /// A bare value where the declared type is a SELECT. ISO 10303-21:2016
81 /// §12.1.8 writes every value of a SELECT that is not an entity instance
82 /// as a typed parameter naming its type.
83 UntypedSelectValue {
84 /// The SELECT the slot declares.
85 select: String,
86 /// What the file wrote, in words.
87 actual: &'static str,
88 },
89}
90
91/// Checks one value against one declared type name.
92///
93/// Returns `None` when the value fits, or when the schema gives no basis to
94/// judge it. "No basis" is deliberately not a finding: an unrecognized type
95/// token means this validator cannot check the slot, and reporting that per
96/// value would bury real defects under noise. The unchecked *rules* are
97/// counted once, in the where-rule registry.
98///
99/// The members of an aggregate are checked against its element type, and
100/// the first mismatching member is reported. The form is judged too: a typed
101/// parameter outside a SELECT, and a bare value inside one. Which *entity* a reference
102/// points at needs the model, so it is judged by
103/// [`crate::structure::wrong_kind_references`] for entity-typed slots and by
104/// [`super::attribute_types`] for SELECTs; this function judges only that a
105/// reference was written where one may be.
106#[must_use]
107pub fn check(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
108 check_nested(schema, declared, value, 0)
109}
110
111fn check_nested(schema: &Schema, declared: &str, value: &Value, depth: usize) -> Option<Mismatch> {
112 match value {
113 // `$` and `*` carry no type; presence is `structure`'s concern.
114 Value::Null | Value::Derived => None,
115 Value::List(items) => {
116 if depth >= MAX_NESTING {
117 return None;
118 }
119 // Whether a list belongs here at all is
120 // `structure::cardinality`'s question; this one is whether its
121 // members fit.
122 let element = element_type(schema, declared);
123 items
124 .iter()
125 .find_map(|item| check_nested(schema, &element, item, depth + 1))
126 }
127 Value::Ref(_) => reference_in_value_slot(schema, declared, value),
128 other if requires_reference(schema, declared, other) => Some(Mismatch::ExpectedReference {
129 declared: declared.to_string(),
130 actual: describe_value(other),
131 }),
132 other
133 if !matches!(other, Value::Typed { .. })
134 && select::resolve_select(schema, declared).is_some() =>
135 {
136 // Every non-entity value of a SELECT names its type (§12.1.8).
137 // A SELECT of entities alone was answered above.
138 Some(Mismatch::UntypedSelectValue {
139 select: declared.to_string(),
140 actual: describe_value(other),
141 })
142 }
143 Value::Enum(member) => match enumeration::is_member(schema, declared, member) {
144 Some(true) | None => None,
145 Some(false) => Some(Mismatch::EnumMember {
146 member: member.to_string(),
147 declared: enumeration::members(schema, declared)
148 .map(<[String]>::to_vec)
149 .unwrap_or_default(),
150 }),
151 },
152 Value::Typed { type_name, value } => typed(schema, declared, type_name, value, depth),
153 Value::Text(text) => {
154 // A fixed-width string is wrong at any other length, even when
155 // it is a perfectly good string. `IfcGloballyUniqueId` is
156 // `STRING(22) FIXED`; a 21-character GUID is malformed.
157 if let Some(width) = FixedWidth::from_resolved(&schema.resolve_defined(declared)) {
158 if !width.accepts(text) {
159 return Some(Mismatch::FixedWidth {
160 expected: width.0,
161 actual: text.chars().count(),
162 });
163 }
164 }
165 primitive_mismatch(schema, declared, value)
166 }
167 other => primitive_mismatch(schema, declared, other),
168 }
169}
170
171/// A typed parameter `type_name(inner)` written in a `declared` slot.
172///
173/// In a SELECT slot the wrapper must name a member of the select-list. In
174/// any other slot a wrapper is the wrong form (§12.1.6): of the wrong type
175/// when it does not name the declared type, and otherwise reported only if
176/// its parameter is sound, so a bad parameter's finding is not masked. The
177/// parameter is judged against the wrapper's type, which is what it claims
178/// to be.
179fn typed(
180 schema: &Schema,
181 declared: &str,
182 type_name: &str,
183 inner: &Value,
184 depth: usize,
185) -> Option<Mismatch> {
186 if select::resolve_select(schema, declared).is_some() {
187 if select::accepts(schema, declared, type_name) == Some(false) {
188 return Some(Mismatch::SelectMember {
189 written: type_name.to_string(),
190 select: declared.to_string(),
191 });
192 }
193 return check_nested(schema, type_name, inner, depth);
194 }
195 match wrapper_fits(schema, type_name, declared) {
196 Some(false) => Some(Mismatch::TypedWrongType {
197 written: type_name.to_string(),
198 declared: declared.to_string(),
199 }),
200 Some(true) => check_nested(schema, type_name, inner, depth).or_else(|| {
201 Some(Mismatch::TypedOutsideSelect {
202 written: type_name.to_string(),
203 declared: declared.to_string(),
204 })
205 }),
206 None => check_nested(schema, type_name, inner, depth),
207 }
208}
209
210/// Whether only an entity reference can fill a `declared` slot, so that
211/// `value` is wrong whatever it holds.
212///
213/// A typed wrapper in a SELECT of entities is left to the SELECT membership
214/// check, which names the wrapper; everywhere else a non-reference in an
215/// entity-typed slot is reported here.
216fn requires_reference(schema: &Schema, declared: &str, value: &Value) -> bool {
217 if schema.entity(declared).is_some() {
218 return true;
219 }
220 !matches!(value, Value::Typed { .. })
221 && select::admits_only_entities(schema, declared) == Some(true)
222}
223
224/// A reference written where the declared type is not an entity.
225///
226/// Entity and SELECT slots take references; a slot resolving to an EXPRESS
227/// primitive does not, and a reference there is a type error however valid
228/// its target is.
229fn reference_in_value_slot(schema: &Schema, declared: &str, reference: &Value) -> Option<Mismatch> {
230 let is_select = select::resolve_select(schema, declared).is_some();
231 if schema.entity(declared).is_some() || is_select {
232 return None;
233 }
234 primitive_mismatch(schema, declared, reference)
235}
236
237/// The value against the primitive `declared` resolves to, if any.
238fn primitive_mismatch(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
239 let primitive = primitive_of(schema, declared)?;
240 if primitive.accepts(value) {
241 None
242 } else {
243 Some(Mismatch::Primitive {
244 expected: primitive.describe(),
245 actual: describe_value(value),
246 })
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use ifc_model::EntityId;
254
255 #[test]
256 fn aggregate_members_are_checked_against_the_element_type() {
257 let schema = ifc_schema::ifc4();
258 let good = Value::List(vec![Value::Real(1.0), Value::Integer(2)]);
259 assert_eq!(check(schema, "IfcLengthMeasure", &good), None);
260 let bad = Value::List(vec![Value::Real(1.0), Value::Text("x".into())]);
261 assert!(matches!(
262 check(schema, "IfcLengthMeasure", &bad),
263 Some(Mismatch::Primitive { .. })
264 ));
265 // An aliased aggregate: IfcLineIndex = LIST [2:?] OF IfcPositiveInteger.
266 let line = Value::List(vec![Value::Integer(1), Value::Real(2.5)]);
267 assert!(matches!(
268 check(schema, "IfcLineIndex", &line),
269 Some(Mismatch::Primitive { .. })
270 ));
271 }
272
273 #[test]
274 fn a_non_reference_in_an_entity_slot_is_a_mismatch() {
275 let schema = ifc_schema::ifc4();
276 for value in [
277 Value::Text("task".into()),
278 Value::Integer(1),
279 Value::Enum("NOTDEFINED".into()),
280 Value::Typed {
281 type_name: "IFCLABEL".into(),
282 value: Box::new(Value::Text("task".into())),
283 },
284 ] {
285 assert!(
286 matches!(
287 check(schema, "IfcProcess", &value),
288 Some(Mismatch::ExpectedReference { .. })
289 ),
290 "{value:?}"
291 );
292 }
293 assert_eq!(check(schema, "IfcProcess", &Value::Ref(EntityId(1))), None);
294 // A SELECT of entities takes only references, too.
295 assert!(matches!(
296 check(schema, "IfcActorSelect", &Value::Text("me".into())),
297 Some(Mismatch::ExpectedReference { .. })
298 ));
299 // A SELECT that also takes values needs the typed form instead
300 // (ISO 10303-21:2016 §12.1.8), which is a different finding.
301 assert!(matches!(
302 check(schema, "IfcValue", &Value::Real(1.0)),
303 Some(Mismatch::UntypedSelectValue { .. })
304 ));
305 }
306
307 #[test]
308 fn a_reference_in_a_value_slot_is_a_mismatch() {
309 let schema = ifc_schema::ifc4();
310 assert!(matches!(
311 check(schema, "IfcLabel", &Value::Ref(EntityId(1))),
312 Some(Mismatch::Primitive { .. })
313 ));
314 assert_eq!(
315 check(schema, "IfcMaterialSelect", &Value::Ref(EntityId(1))),
316 None
317 );
318 }
319}