1use ifc_model::Value;
4use ifc_schema::{Schema, TypeKind};
5
6use super::aggregate::element_type;
7use super::enumeration;
8use super::scalar::{describe_value, primitive_of, FixedWidth};
9use super::select;
10
11const MAX_NESTING: usize = 8;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum Mismatch {
22 Primitive {
24 expected: &'static str,
26 actual: &'static str,
28 },
29 EnumMember {
31 member: String,
33 declared: Vec<String>,
35 },
36 SelectMember {
38 written: String,
40 select: String,
42 },
43 FixedWidth {
45 expected: usize,
47 actual: usize,
49 },
50 ExpectedReference {
53 declared: String,
55 actual: &'static str,
57 },
58}
59
60#[must_use]
75pub fn check(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
76 check_nested(schema, declared, value, 0)
77}
78
79fn check_nested(schema: &Schema, declared: &str, value: &Value, depth: usize) -> Option<Mismatch> {
80 match value {
81 Value::Null | Value::Derived => None,
83 Value::List(items) => {
84 if depth >= MAX_NESTING {
85 return None;
86 }
87 let element = element_type(schema, declared);
91 items
92 .iter()
93 .find_map(|item| check_nested(schema, &element, item, depth + 1))
94 }
95 Value::Ref(_) => reference_in_value_slot(schema, declared, value),
96 other if requires_reference(schema, declared, other) => Some(Mismatch::ExpectedReference {
97 declared: declared.to_string(),
98 actual: describe_value(other),
99 }),
100 Value::Enum(member) => match enumeration::is_member(schema, declared, member) {
101 Some(true) | None => None,
102 Some(false) => Some(Mismatch::EnumMember {
103 member: member.to_string(),
104 declared: enumeration::members(schema, declared)
105 .map(<[String]>::to_vec)
106 .unwrap_or_default(),
107 }),
108 },
109 Value::Typed { type_name, value } => {
110 if let Some(false) = select::accepts(schema, declared, type_name) {
112 return Some(Mismatch::SelectMember {
113 written: type_name.to_string(),
114 select: declared.to_string(),
115 });
116 }
117 check_nested(schema, type_name, value, depth)
120 }
121 Value::Text(text) => {
122 if let Some(width) = FixedWidth::from_resolved(&schema.resolve_defined(declared)) {
126 if !width.accepts(text) {
127 return Some(Mismatch::FixedWidth {
128 expected: width.0,
129 actual: text.chars().count(),
130 });
131 }
132 }
133 primitive_mismatch(schema, declared, value)
134 }
135 other => primitive_mismatch(schema, declared, other),
136 }
137}
138
139fn requires_reference(schema: &Schema, declared: &str, value: &Value) -> bool {
146 if schema.entity(declared).is_some() {
147 return true;
148 }
149 !matches!(value, Value::Typed { .. })
150 && select::admits_only_entities(schema, declared) == Some(true)
151}
152
153fn reference_in_value_slot(schema: &Schema, declared: &str, reference: &Value) -> Option<Mismatch> {
159 let is_select = schema
160 .type_def(declared)
161 .is_some_and(|definition| matches!(definition.kind, TypeKind::Select(_)));
162 if schema.entity(declared).is_some() || is_select {
163 return None;
164 }
165 primitive_mismatch(schema, declared, reference)
166}
167
168fn primitive_mismatch(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
170 let primitive = primitive_of(schema, declared)?;
171 if primitive.accepts(value) {
172 None
173 } else {
174 Some(Mismatch::Primitive {
175 expected: primitive.describe(),
176 actual: describe_value(value),
177 })
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use ifc_model::EntityId;
185
186 #[test]
187 fn aggregate_members_are_checked_against_the_element_type() {
188 let schema = ifc_schema::ifc4();
189 let good = Value::List(vec![Value::Real(1.0), Value::Integer(2)]);
190 assert_eq!(check(schema, "IfcLengthMeasure", &good), None);
191 let bad = Value::List(vec![Value::Real(1.0), Value::Text("x".into())]);
192 assert!(matches!(
193 check(schema, "IfcLengthMeasure", &bad),
194 Some(Mismatch::Primitive { .. })
195 ));
196 let line = Value::List(vec![Value::Integer(1), Value::Real(2.5)]);
198 assert!(matches!(
199 check(schema, "IfcLineIndex", &line),
200 Some(Mismatch::Primitive { .. })
201 ));
202 }
203
204 #[test]
205 fn a_non_reference_in_an_entity_slot_is_a_mismatch() {
206 let schema = ifc_schema::ifc4();
207 for value in [
208 Value::Text("task".into()),
209 Value::Integer(1),
210 Value::Enum("NOTDEFINED".into()),
211 Value::Typed {
212 type_name: "IFCLABEL".into(),
213 value: Box::new(Value::Text("task".into())),
214 },
215 ] {
216 assert!(
217 matches!(
218 check(schema, "IfcProcess", &value),
219 Some(Mismatch::ExpectedReference { .. })
220 ),
221 "{value:?}"
222 );
223 }
224 assert_eq!(check(schema, "IfcProcess", &Value::Ref(EntityId(1))), None);
225 assert!(matches!(
227 check(schema, "IfcActorSelect", &Value::Text("me".into())),
228 Some(Mismatch::ExpectedReference { .. })
229 ));
230 assert_eq!(check(schema, "IfcValue", &Value::Real(1.0)), None);
232 }
233
234 #[test]
235 fn a_reference_in_a_value_slot_is_a_mismatch() {
236 let schema = ifc_schema::ifc4();
237 assert!(matches!(
238 check(schema, "IfcLabel", &Value::Ref(EntityId(1))),
239 Some(Mismatch::Primitive { .. })
240 ));
241 assert_eq!(
242 check(schema, "IfcMaterialSelect", &Value::Ref(EntityId(1))),
243 None
244 );
245 }
246}