Skip to main content

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/// Returns the first of [`check_all`]'s mismatches; use that function to
100/// see every independent violation in the value.
101#[must_use]
102pub fn check(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
103    check_all(schema, declared, value).into_iter().next()
104}
105
106/// Every independent way one value fails one declared type name, in the
107/// order the value is written.
108///
109/// The members of an aggregate are each checked against its element type,
110/// at every nesting level. The form is judged too: a typed parameter outside
111/// a SELECT, and a bare value inside one. A wrapper's form and its
112/// parameter are independent: `IFCLABEL(12)` in an `IfcLabel` slot is both
113/// the wrong form and a number where a string is declared, and both are
114/// returned. Identical mismatches are returned once, since a finding cannot
115/// tell them apart. Which *entity* a reference points at needs the model,
116/// so it is judged by [`crate::structure::wrong_kind_references`] for
117/// entity-typed slots and by [`super::attribute_types`] for SELECTs; this
118/// function judges only that a reference was written where one may be.
119#[must_use]
120pub fn check_all(schema: &Schema, declared: &str, value: &Value) -> Vec<Mismatch> {
121    let mut found = Vec::new();
122    check_nested(schema, declared, value, 0, &mut found);
123    found
124}
125
126/// Records `mismatch` unless an identical one already is.
127fn record(found: &mut Vec<Mismatch>, mismatch: Mismatch) {
128    if !found.contains(&mismatch) {
129        found.push(mismatch);
130    }
131}
132
133fn check_nested(
134    schema: &Schema,
135    declared: &str,
136    value: &Value,
137    depth: usize,
138    found: &mut Vec<Mismatch>,
139) {
140    let mismatch = match value {
141        // `$` and `*` carry no type; presence is `structure`'s concern.
142        Value::Null | Value::Derived => None,
143        Value::List(items) => {
144            if depth >= MAX_NESTING {
145                return;
146            }
147            // Whether a list belongs here at all is
148            // `structure::cardinality`'s question; this one is whether its
149            // members fit.
150            let element = element_type(schema, declared);
151            for item in items {
152                check_nested(schema, &element, item, depth + 1, found);
153            }
154            None
155        }
156        Value::Ref(_) => reference_in_value_slot(schema, declared, value),
157        other if requires_reference(schema, declared, other) => Some(Mismatch::ExpectedReference {
158            declared: declared.to_string(),
159            actual: describe_value(other),
160        }),
161        other
162            if !matches!(other, Value::Typed { .. })
163                && select::resolve_select(schema, declared).is_some() =>
164        {
165            // Every non-entity value of a SELECT names its type (§12.1.8).
166            // A SELECT of entities alone was answered above.
167            Some(Mismatch::UntypedSelectValue {
168                select: declared.to_string(),
169                actual: describe_value(other),
170            })
171        }
172        Value::Enum(member) => match enumeration::is_member(schema, declared, member) {
173            Some(true) | None => None,
174            Some(false) => Some(Mismatch::EnumMember {
175                member: member.to_string(),
176                declared: enumeration::members(schema, declared)
177                    .map(<[String]>::to_vec)
178                    .unwrap_or_default(),
179            }),
180        },
181        Value::Typed { type_name, value } => {
182            typed(schema, declared, type_name, value, depth, found);
183            None
184        }
185        Value::Text(text) => {
186            // A fixed-width string is wrong at any other length, even when
187            // it is a perfectly good string. `IfcGloballyUniqueId` is
188            // `STRING(22) FIXED`; a 21-character GUID is malformed.
189            match FixedWidth::from_resolved(&schema.resolve_defined(declared)) {
190                Some(width) if !width.accepts(text) => Some(Mismatch::FixedWidth {
191                    expected: width.0,
192                    actual: text.chars().count(),
193                }),
194                _ => primitive_mismatch(schema, declared, value),
195            }
196        }
197        other => primitive_mismatch(schema, declared, other),
198    };
199    if let Some(mismatch) = mismatch {
200        record(found, mismatch);
201    }
202}
203
204/// A typed parameter `type_name(inner)` written in a `declared` slot.
205///
206/// In a SELECT slot the wrapper must name a member of the select-list. In
207/// any other slot a wrapper is the wrong form (§12.1.6), and of the wrong
208/// type too when it does not name the declared type. Either way the
209/// parameter is judged as well, against the wrapper's type, which is what
210/// it claims to be: a bad parameter is a separate violation, and neither
211/// finding masks the other.
212fn typed(
213    schema: &Schema,
214    declared: &str,
215    type_name: &str,
216    inner: &Value,
217    depth: usize,
218    found: &mut Vec<Mismatch>,
219) {
220    if select::resolve_select(schema, declared).is_some() {
221        if select::accepts(schema, declared, type_name) == Some(false) {
222            record(
223                found,
224                Mismatch::SelectMember {
225                    written: type_name.to_string(),
226                    select: declared.to_string(),
227                },
228            );
229        }
230        check_nested(schema, type_name, inner, depth, found);
231        return;
232    }
233    match wrapper_fits(schema, type_name, declared) {
234        Some(false) => record(
235            found,
236            Mismatch::TypedWrongType {
237                written: type_name.to_string(),
238                declared: declared.to_string(),
239            },
240        ),
241        Some(true) => record(
242            found,
243            Mismatch::TypedOutsideSelect {
244                written: type_name.to_string(),
245                declared: declared.to_string(),
246            },
247        ),
248        None => {}
249    }
250    check_nested(schema, type_name, inner, depth, found);
251}
252
253/// Whether only an entity reference can fill a `declared` slot, so that
254/// `value` is wrong whatever it holds.
255///
256/// A typed wrapper in a SELECT of entities is left to the SELECT membership
257/// check, which names the wrapper; everywhere else a non-reference in an
258/// entity-typed slot is reported here.
259fn requires_reference(schema: &Schema, declared: &str, value: &Value) -> bool {
260    if schema.entity(declared).is_some() {
261        return true;
262    }
263    !matches!(value, Value::Typed { .. })
264        && select::admits_only_entities(schema, declared) == Some(true)
265}
266
267/// A reference written where the declared type is not an entity.
268///
269/// Entity and SELECT slots take references; a slot resolving to an EXPRESS
270/// primitive does not, and a reference there is a type error however valid
271/// its target is.
272fn reference_in_value_slot(schema: &Schema, declared: &str, reference: &Value) -> Option<Mismatch> {
273    let is_select = select::resolve_select(schema, declared).is_some();
274    if schema.entity(declared).is_some() || is_select {
275        return None;
276    }
277    primitive_mismatch(schema, declared, reference)
278}
279
280/// The value against the primitive `declared` resolves to, if any.
281fn primitive_mismatch(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
282    let primitive = primitive_of(schema, declared)?;
283    if primitive.accepts(value) {
284        None
285    } else {
286        Some(Mismatch::Primitive {
287            expected: primitive.describe(),
288            actual: describe_value(value),
289        })
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use ifc_model::EntityId;
297
298    #[test]
299    fn aggregate_members_are_checked_against_the_element_type() {
300        let schema = ifc_schema::ifc4();
301        let good = Value::List(vec![Value::Real(1.0), Value::Integer(2)]);
302        assert_eq!(check(schema, "IfcLengthMeasure", &good), None);
303        let bad = Value::List(vec![Value::Real(1.0), Value::Text("x".into())]);
304        assert!(matches!(
305            check(schema, "IfcLengthMeasure", &bad),
306            Some(Mismatch::Primitive { .. })
307        ));
308        // An aliased aggregate: IfcLineIndex = LIST [2:?] OF IfcPositiveInteger.
309        let line = Value::List(vec![Value::Integer(1), Value::Real(2.5)]);
310        assert!(matches!(
311            check(schema, "IfcLineIndex", &line),
312            Some(Mismatch::Primitive { .. })
313        ));
314    }
315
316    #[test]
317    fn a_non_reference_in_an_entity_slot_is_a_mismatch() {
318        let schema = ifc_schema::ifc4();
319        for value in [
320            Value::Text("task".into()),
321            Value::Integer(1),
322            Value::Enum("NOTDEFINED".into()),
323            Value::Typed {
324                type_name: "IFCLABEL".into(),
325                value: Box::new(Value::Text("task".into())),
326            },
327        ] {
328            assert!(
329                matches!(
330                    check(schema, "IfcProcess", &value),
331                    Some(Mismatch::ExpectedReference { .. })
332                ),
333                "{value:?}"
334            );
335        }
336        assert_eq!(check(schema, "IfcProcess", &Value::Ref(EntityId(1))), None);
337        // A SELECT of entities takes only references, too.
338        assert!(matches!(
339            check(schema, "IfcActorSelect", &Value::Text("me".into())),
340            Some(Mismatch::ExpectedReference { .. })
341        ));
342        // A SELECT that also takes values needs the typed form instead
343        // (ISO 10303-21:2016 §12.1.8), which is a different finding.
344        assert!(matches!(
345            check(schema, "IfcValue", &Value::Real(1.0)),
346            Some(Mismatch::UntypedSelectValue { .. })
347        ));
348    }
349
350    #[test]
351    fn a_reference_in_a_value_slot_is_a_mismatch() {
352        let schema = ifc_schema::ifc4();
353        assert!(matches!(
354            check(schema, "IfcLabel", &Value::Ref(EntityId(1))),
355            Some(Mismatch::Primitive { .. })
356        ));
357        assert_eq!(
358            check(schema, "IfcMaterialSelect", &Value::Ref(EntityId(1))),
359            None
360        );
361    }
362
363    /// Each bad member of an aggregate is its own violation, at every
364    /// nesting level (#215).
365    #[test]
366    fn every_bad_member_of_an_aggregate_is_reported() {
367        let schema = ifc_schema::ifc4();
368        let bad = Value::List(vec![
369            Value::Text("x".into()),
370            Value::Real(1.0),
371            Value::Bool(true),
372        ]);
373        let found = check_all(schema, "IfcLengthMeasure", &bad);
374        assert_eq!(found.len(), 2, "{found:?}");
375        // `IfcCartesianPointList3D.CoordList` is `LIST OF LIST OF
376        // IfcLengthMeasure`: two rows, each with its own defect.
377        let rows = Value::List(vec![
378            Value::List(vec![Value::Text("x".into()), Value::Real(0.0)]),
379            Value::List(vec![Value::Real(0.0), Value::Bool(false)]),
380        ]);
381        assert_eq!(check_all(schema, "IfcLengthMeasure", &rows).len(), 2);
382        // The same defect twice cannot be told apart in a finding.
383        let twice = Value::List(vec![Value::Text("x".into()), Value::Text("y".into())]);
384        assert_eq!(check_all(schema, "IfcLengthMeasure", &twice).len(), 1);
385        // `check` still answers the first.
386        assert!(matches!(
387            check(schema, "IfcLengthMeasure", &bad),
388            Some(Mismatch::Primitive {
389                actual: "a string",
390                ..
391            })
392        ));
393    }
394
395    /// A wrapper's form and its parameter are judged independently, so a
396    /// bad payload inside a wrapper does not hide the wrong form (#215).
397    #[test]
398    fn a_wrong_form_and_a_bad_payload_are_both_reported() {
399        let schema = ifc_schema::ifc4();
400        let wrapped = Value::Typed {
401            type_name: "IFCLABEL".into(),
402            value: Box::new(Value::Integer(12)),
403        };
404        let found = check_all(schema, "IfcLabel", &wrapped);
405        assert!(
406            found
407                .iter()
408                .any(|m| matches!(m, Mismatch::TypedOutsideSelect { .. })),
409            "{found:?}"
410        );
411        assert!(
412            found
413                .iter()
414                .any(|m| matches!(m, Mismatch::Primitive { .. })),
415            "{found:?}"
416        );
417        // A wrapper outside its SELECT, around a payload its own type
418        // refuses: both the membership and the payload.
419        let foreign = Value::Typed {
420            type_name: "IFCLABEL".into(),
421            value: Box::new(Value::Real(1.0)),
422        };
423        let found = check_all(schema, "IfcMeasureValue", &foreign);
424        assert!(
425            found
426                .iter()
427                .any(|m| matches!(m, Mismatch::SelectMember { .. })),
428            "{found:?}"
429        );
430        assert!(
431            found
432                .iter()
433                .any(|m| matches!(m, Mismatch::Primitive { .. })),
434            "{found:?}"
435        );
436    }
437}