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/// 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}