use std::collections::BTreeSet;
use ifc_schema::{Schema, TypeKind};
const MAX_VISITED: usize = 4096;
const MAX_ALIAS_HOPS: usize = 16;
#[must_use]
pub fn resolve_select(schema: &Schema, type_name: &str) -> Option<String> {
let mut current = type_name.trim().to_string();
for _ in 0..MAX_ALIAS_HOPS {
match &schema.type_def(¤t)?.kind {
TypeKind::Select(_) => return Some(current),
TypeKind::Defined(target) => current = target.trim().to_string(),
TypeKind::Enumeration(_) => return None,
}
}
None
}
#[must_use]
pub fn accepts(schema: &Schema, type_name: &str, candidate: &str) -> Option<bool> {
let select = resolve_select(schema, type_name)?;
let mut frontier = vec![select.to_ascii_uppercase()];
let mut seen: BTreeSet<String> = BTreeSet::new();
while let Some(current) = frontier.pop() {
if !seen.insert(current.clone()) {
continue;
}
if seen.len() > MAX_VISITED {
return None;
}
let Some(TypeKind::Select(members)) =
schema.type_def(¤t).map(|definition| &definition.kind)
else {
continue;
};
for member in members {
if member.eq_ignore_ascii_case(candidate) {
return Some(true);
}
frontier.push(member.to_ascii_uppercase());
}
}
Some(false)
}
struct EntityMembers {
entities: Vec<String>,
values: bool,
unknown: bool,
}
fn entity_members(schema: &Schema, type_name: &str) -> Option<EntityMembers> {
let select = resolve_select(schema, type_name)?;
let mut members = EntityMembers {
entities: Vec::new(),
values: false,
unknown: false,
};
let mut frontier = vec![select.to_ascii_uppercase()];
let mut seen: BTreeSet<String> = BTreeSet::new();
while let Some(current) = frontier.pop() {
if !seen.insert(current.clone()) {
continue;
}
if seen.len() > MAX_VISITED {
return None;
}
if schema.entity(¤t).is_some() {
members.entities.push(current);
continue;
}
match schema.type_def(¤t).map(|definition| &definition.kind) {
Some(TypeKind::Select(nested)) => {
frontier.extend(nested.iter().map(|member| member.to_ascii_uppercase()));
}
Some(TypeKind::Defined(_) | TypeKind::Enumeration(_)) => members.values = true,
None => members.unknown = true,
}
}
Some(members)
}
#[must_use]
pub fn admits_entity(schema: &Schema, type_name: &str, entity_type: &str) -> Option<bool> {
let members = entity_members(schema, type_name)?;
if members
.entities
.iter()
.any(|entity| schema.is_a(entity_type, entity))
{
Some(true)
} else if members.unknown {
None
} else {
Some(false)
}
}
#[must_use]
pub fn admits_only_entities(schema: &Schema, type_name: &str) -> Option<bool> {
let members = entity_members(schema, type_name)?;
(!members.unknown).then_some(!members.values)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entity_membership_walks_nested_selects_and_subtypes() {
let schema = ifc_schema::ifc4();
assert_eq!(
admits_entity(schema, "IfcMaterialSelect", "IfcMaterial"),
Some(true)
);
assert_eq!(
admits_entity(schema, "IfcMaterialSelect", "IfcWall"),
Some(false)
);
assert_eq!(
admits_entity(schema, "IfcDefinitionSelect", "IfcWall"),
Some(true)
);
assert_eq!(
admits_entity(
schema,
"IfcCurveFontOrScaledCurveFontSelect",
"IfcCurveStyleFont"
),
Some(true)
);
assert_eq!(admits_entity(schema, "IfcLabel", "IfcWall"), None);
}
#[test]
fn entity_only_selects_are_recognised() {
let schema = ifc_schema::ifc4();
assert_eq!(admits_only_entities(schema, "IfcActorSelect"), Some(true));
assert_eq!(admits_only_entities(schema, "IfcValue"), Some(false));
assert_eq!(
admits_only_entities(schema, "IfcPropertySetDefinitionSelect"),
Some(false)
);
}
#[test]
fn a_member_behind_a_wide_select_is_found() {
let schema = ifc_schema::ifc4();
assert_eq!(
accepts(schema, "IfcAppliedValueSelect", "IfcMonetaryMeasure"),
Some(true),
);
}
#[test]
fn a_derived_measure_is_a_member_of_ifc_value() {
let schema = ifc_schema::ifc4();
assert_eq!(
accepts(schema, "IfcValue", "IfcVolumetricFlowRateMeasure"),
Some(true),
);
}
#[test]
fn a_non_member_is_still_rejected() {
let schema = ifc_schema::ifc4();
assert_eq!(accepts(schema, "IfcValue", "IfcWall"), Some(false));
}
#[test]
fn every_bundled_select_closure_completes_within_the_visit_bound() {
for schema in [
ifc_schema::ifc2x3(),
ifc_schema::ifc4(),
ifc_schema::ifc4x3(),
] {
let mut selects = BTreeSet::new();
for entity in schema.entity_names() {
for attribute in schema.attributes(entity) {
if let Some(definition) = schema.type_def(&attribute.type_name) {
if matches!(definition.kind, TypeKind::Select(_)) {
selects.insert(attribute.type_name.clone());
}
}
}
}
assert!(selects.len() > 20, "{}: {selects:?}", schema.name());
for select in &selects {
assert_eq!(
accepts(schema, select, "NOT_A_DECLARED_TYPE"),
Some(false),
"{}: the walk over {select} did not complete",
schema.name()
);
}
}
}
#[test]
fn a_non_select_returns_none() {
let schema = ifc_schema::ifc4();
assert_eq!(accepts(schema, "IfcLengthMeasure", "REAL"), None);
}
}