Skip to main content

ifc_validate/type_check/
defined.rs

1//! Values checked against the defined type declared for their slot.
2
3use ifc_model::Value;
4use ifc_schema::Schema;
5
6use super::enumeration;
7use super::scalar::{describe_value, primitive_of, FixedWidth};
8use super::select;
9
10/// Why a value does not fit its declared type.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Mismatch {
13    /// The written form is not what the primitive accepts.
14    Primitive {
15        /// What the schema expects, in words.
16        expected: &'static str,
17        /// What the file wrote, in words.
18        actual: &'static str,
19    },
20    /// An enumeration constant the schema does not declare.
21    EnumMember {
22        /// The constant as written.
23        member: String,
24        /// The members the schema declares.
25        declared: Vec<String>,
26    },
27    /// A typed value whose type is not a member of the declared SELECT.
28    SelectMember {
29        /// The wrapper type as written.
30        written: String,
31        /// The SELECT it had to belong to.
32        select: String,
33    },
34    /// A `STRING(n) FIXED` value written at the wrong width.
35    FixedWidth {
36        /// The width the schema fixes.
37        expected: usize,
38        /// The width actually written.
39        actual: usize,
40    },
41}
42
43/// Checks one value against one declared type name.
44///
45/// Returns `None` when the value fits, or when the schema gives no basis to
46/// judge it. "No basis" is deliberately not a finding: an unrecognized type
47/// token means this validator cannot check the slot, and reporting that per
48/// value would bury real defects under noise. The unchecked *rules* are
49/// counted once, in the where-rule registry.
50#[must_use]
51pub fn check(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
52    match value {
53        // `$` and `*` carry no type; presence is `structure`'s concern.
54        Value::Null | Value::Derived => None,
55        Value::Enum(member) => match enumeration::is_member(schema, declared, member) {
56            Some(true) | None => None,
57            Some(false) => Some(Mismatch::EnumMember {
58                member: member.to_string(),
59                declared: enumeration::members(schema, declared)
60                    .map(<[String]>::to_vec)
61                    .unwrap_or_default(),
62            }),
63        },
64        Value::Typed { type_name, value } => {
65            // A typed wrapper in a SELECT slot must name a SELECT member.
66            if let Some(false) = select::accepts(schema, declared, type_name) {
67                return Some(Mismatch::SelectMember {
68                    written: type_name.to_string(),
69                    select: declared.to_string(),
70                });
71            }
72            // Otherwise the wrapper names the real type: check the payload
73            // against it rather than against the slot's declared type.
74            check(schema, type_name, value)
75        }
76        // A reference's target type is checked structurally, where the model
77        // is available to resolve it.
78        Value::Ref(_) => None,
79        // Aggregate shape is `structure::cardinality`'s concern; element
80        // types are not declared by the parser's flat type token.
81        Value::List(_) => None,
82        Value::Text(text) => {
83            // A fixed-width string is wrong at any other length, even when
84            // it is a perfectly good string. `IfcGloballyUniqueId` is
85            // `STRING(22) FIXED`; a 21-character GUID is malformed.
86            if let Some(width) = FixedWidth::from_resolved(&schema.resolve_defined(declared)) {
87                if !width.accepts(text) {
88                    return Some(Mismatch::FixedWidth {
89                        expected: width.0,
90                        actual: text.chars().count(),
91                    });
92                }
93            }
94            let primitive = primitive_of(schema, declared)?;
95            if primitive.accepts(value) {
96                None
97            } else {
98                Some(Mismatch::Primitive {
99                    expected: primitive.describe(),
100                    actual: describe_value(value),
101                })
102            }
103        }
104        other => {
105            let primitive = primitive_of(schema, declared)?;
106            if primitive.accepts(other) {
107                None
108            } else {
109                Some(Mismatch::Primitive {
110                    expected: primitive.describe(),
111                    actual: describe_value(other),
112                })
113            }
114        }
115    }
116}