use ifc_model::Value;
use ifc_schema::{Schema, TypeKind};
use super::aggregate::element_type;
use super::enumeration;
use super::scalar::{describe_value, primitive_of, FixedWidth};
use super::select;
const MAX_NESTING: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Mismatch {
Primitive {
expected: &'static str,
actual: &'static str,
},
EnumMember {
member: String,
declared: Vec<String>,
},
SelectMember {
written: String,
select: String,
},
FixedWidth {
expected: usize,
actual: usize,
},
ExpectedReference {
declared: String,
actual: &'static str,
},
}
#[must_use]
pub fn check(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
check_nested(schema, declared, value, 0)
}
fn check_nested(schema: &Schema, declared: &str, value: &Value, depth: usize) -> Option<Mismatch> {
match value {
Value::Null | Value::Derived => None,
Value::List(items) => {
if depth >= MAX_NESTING {
return None;
}
let element = element_type(schema, declared);
items
.iter()
.find_map(|item| check_nested(schema, &element, item, depth + 1))
}
Value::Ref(_) => reference_in_value_slot(schema, declared, value),
other if requires_reference(schema, declared, other) => Some(Mismatch::ExpectedReference {
declared: declared.to_string(),
actual: describe_value(other),
}),
Value::Enum(member) => match enumeration::is_member(schema, declared, member) {
Some(true) | None => None,
Some(false) => Some(Mismatch::EnumMember {
member: member.to_string(),
declared: enumeration::members(schema, declared)
.map(<[String]>::to_vec)
.unwrap_or_default(),
}),
},
Value::Typed { type_name, value } => {
if let Some(false) = select::accepts(schema, declared, type_name) {
return Some(Mismatch::SelectMember {
written: type_name.to_string(),
select: declared.to_string(),
});
}
check_nested(schema, type_name, value, depth)
}
Value::Text(text) => {
if let Some(width) = FixedWidth::from_resolved(&schema.resolve_defined(declared)) {
if !width.accepts(text) {
return Some(Mismatch::FixedWidth {
expected: width.0,
actual: text.chars().count(),
});
}
}
primitive_mismatch(schema, declared, value)
}
other => primitive_mismatch(schema, declared, other),
}
}
fn requires_reference(schema: &Schema, declared: &str, value: &Value) -> bool {
if schema.entity(declared).is_some() {
return true;
}
!matches!(value, Value::Typed { .. })
&& select::admits_only_entities(schema, declared) == Some(true)
}
fn reference_in_value_slot(schema: &Schema, declared: &str, reference: &Value) -> Option<Mismatch> {
let is_select = schema
.type_def(declared)
.is_some_and(|definition| matches!(definition.kind, TypeKind::Select(_)));
if schema.entity(declared).is_some() || is_select {
return None;
}
primitive_mismatch(schema, declared, reference)
}
fn primitive_mismatch(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
let primitive = primitive_of(schema, declared)?;
if primitive.accepts(value) {
None
} else {
Some(Mismatch::Primitive {
expected: primitive.describe(),
actual: describe_value(value),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::EntityId;
#[test]
fn aggregate_members_are_checked_against_the_element_type() {
let schema = ifc_schema::ifc4();
let good = Value::List(vec![Value::Real(1.0), Value::Integer(2)]);
assert_eq!(check(schema, "IfcLengthMeasure", &good), None);
let bad = Value::List(vec![Value::Real(1.0), Value::Text("x".into())]);
assert!(matches!(
check(schema, "IfcLengthMeasure", &bad),
Some(Mismatch::Primitive { .. })
));
let line = Value::List(vec![Value::Integer(1), Value::Real(2.5)]);
assert!(matches!(
check(schema, "IfcLineIndex", &line),
Some(Mismatch::Primitive { .. })
));
}
#[test]
fn a_non_reference_in_an_entity_slot_is_a_mismatch() {
let schema = ifc_schema::ifc4();
for value in [
Value::Text("task".into()),
Value::Integer(1),
Value::Enum("NOTDEFINED".into()),
Value::Typed {
type_name: "IFCLABEL".into(),
value: Box::new(Value::Text("task".into())),
},
] {
assert!(
matches!(
check(schema, "IfcProcess", &value),
Some(Mismatch::ExpectedReference { .. })
),
"{value:?}"
);
}
assert_eq!(check(schema, "IfcProcess", &Value::Ref(EntityId(1))), None);
assert!(matches!(
check(schema, "IfcActorSelect", &Value::Text("me".into())),
Some(Mismatch::ExpectedReference { .. })
));
assert_eq!(check(schema, "IfcValue", &Value::Real(1.0)), None);
}
#[test]
fn a_reference_in_a_value_slot_is_a_mismatch() {
let schema = ifc_schema::ifc4();
assert!(matches!(
check(schema, "IfcLabel", &Value::Ref(EntityId(1))),
Some(Mismatch::Primitive { .. })
));
assert_eq!(
check(schema, "IfcMaterialSelect", &Value::Ref(EntityId(1))),
None
);
}
}