use ifc_schema::{Schema, SchemaVersion};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Support {
Implemented,
Unsupported(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RuleEntry {
pub id: &'static str,
pub entity: Option<&'static str>,
pub releases: &'static [SchemaVersion],
pub support: Support,
}
impl RuleEntry {
#[must_use]
pub fn applies_to(&self, schema: &Schema) -> bool {
schema
.version()
.is_some_and(|version| self.releases.contains(&version))
}
}
const NEEDS_EXPRESSIONS: &str = "requires an EXPRESS expression evaluator";
const NEEDS_BOUNDS: &str = "requires aggregate bounds, which the schema parser does not retain";
const NEEDS_INVERSES: &str =
"not implemented uniformly: IFC2X3 requires INVERSE relationship semantics, which validation does not derive";
const NEEDS_GEOMETRY: &str = "requires geometric evaluation, which validation does not perform";
const ALL: &[SchemaVersion] = &[
SchemaVersion::Ifc2x3,
SchemaVersion::Ifc4,
SchemaVersion::Ifc4x3,
];
const IFC2X3: &[SchemaVersion] = &[SchemaVersion::Ifc2x3];
const IFC4_FAMILY: &[SchemaVersion] = &[SchemaVersion::Ifc4, SchemaVersion::Ifc4x3];
pub const RULES: &[RuleEntry] = &[
RuleEntry {
id: "global.IfcSingleProjectInstance",
entity: None,
releases: ALL,
support: Support::Implemented,
},
RuleEntry {
id: "global.UniqueGlobalId",
entity: None,
releases: ALL,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelDefinesByProperties.NoRelatedTypeObject",
entity: Some("IfcRelDefinesByProperties"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcExternalReference.WR1",
entity: Some("IfcExternalReference"),
releases: ALL,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelSequence.WR1",
entity: Some("IfcRelSequence"),
releases: IFC2X3,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelSequence.AvoidInconsistentSequence",
entity: Some("IfcRelSequence"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelAggregates.NoSelfReference",
entity: Some("IfcRelAggregates"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelNests.NoSelfReference",
entity: Some("IfcRelNests"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcMaterialLayer.NormalizedPriority",
entity: Some("IfcMaterialLayer"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelAssignsToActor.NoSelfReference",
entity: Some("IfcRelAssignsToActor"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelAssignsToProcess.NoSelfReference",
entity: Some("IfcRelAssignsToProcess"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelAssignsToProduct.NoSelfReference",
entity: Some("IfcRelAssignsToProduct"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelAssignsToGroup.NoSelfReference",
entity: Some("IfcRelAssignsToGroup"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelConnectsPathElements.NormalizedRelatingPriorities",
entity: Some("IfcRelConnectsPathElements"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelConnectsPathElements.NormalizedRelatedPriorities",
entity: Some("IfcRelConnectsPathElements"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcRelSpaceBoundary.CorrectPhysOrVirt",
entity: Some("IfcRelSpaceBoundary"),
releases: IFC4_FAMILY,
support: Support::Implemented,
},
RuleEntry {
id: "IfcDocumentReference.WR1",
entity: Some("IfcDocumentReference"),
releases: ALL,
support: Support::Unsupported(NEEDS_INVERSES),
},
RuleEntry {
id: "IfcRepresentationContextSameWCS",
entity: None,
releases: ALL,
support: Support::Unsupported(NEEDS_GEOMETRY),
},
RuleEntry {
id: "IfcPolyLoop.WR21",
entity: Some("IfcPolyLoop"),
releases: IFC2X3,
support: Support::Unsupported(NEEDS_BOUNDS),
},
RuleEntry {
id: "IfcPolyLoop.AllPointsSameDim",
entity: Some("IfcPolyLoop"),
releases: IFC4_FAMILY,
support: Support::Unsupported(NEEDS_BOUNDS),
},
RuleEntry {
id: "IfcQuantityLength.WR21",
entity: Some("IfcQuantityLength"),
releases: ALL,
support: Support::Unsupported(NEEDS_EXPRESSIONS),
},
RuleEntry {
id: "IfcZone.WR1",
entity: Some("IfcZone"),
releases: ALL,
support: Support::Unsupported(NEEDS_EXPRESSIONS),
},
];
#[must_use]
pub fn lookup(id: &str) -> Option<&'static RuleEntry> {
RULES.iter().find(|entry| entry.id == id)
}
pub fn unsupported() -> impl Iterator<Item = &'static RuleEntry> {
RULES
.iter()
.filter(|entry| matches!(entry.support, Support::Unsupported(_)))
}
pub fn implemented() -> impl Iterator<Item = &'static RuleEntry> {
RULES
.iter()
.filter(|entry| matches!(entry.support, Support::Implemented))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rule_ids_are_unique() {
let mut ids: Vec<&str> = RULES.iter().map(|entry| entry.id).collect();
ids.sort_unstable();
let count = ids.len();
ids.dedup();
assert_eq!(ids.len(), count, "duplicate rule id in the registry");
}
#[test]
fn the_registry_admits_what_it_cannot_check() {
assert!(
unsupported().count() > 0,
"a validator claiming full WHERE-rule coverage is lying"
);
}
#[test]
fn entity_ids_name_their_declaring_entity_in_every_claimed_release() {
for entry in RULES {
assert!(!entry.releases.is_empty(), "{} applies nowhere", entry.id);
let Some(entity) = entry.entity else {
continue;
};
assert!(
entry.id.starts_with(&format!("{entity}.")),
"{} is not `{entity}.<label>`",
entry.id
);
for version in entry.releases {
let schema = ifc_schema::for_version(*version).expect("bundled tables");
assert!(
schema.entity(entity).is_some(),
"{} claims {version:?}, which declares no {entity}",
entry.id
);
}
}
}
#[test]
fn an_unknown_release_declares_no_registered_rule() {
let schema = Schema::from_express("SCHEMA IFC9;\nEND_SCHEMA;\n");
assert!(RULES.iter().all(|entry| !entry.applies_to(&schema)));
}
#[test]
fn unsupported_boundaries_remain_explicit() {
let reasons: Vec<_> = unsupported()
.filter_map(|entry| match entry.support {
Support::Unsupported(reason) => Some(reason),
Support::Implemented => None,
})
.collect();
for required in ["aggregate bounds", "EXPRESS expression", "INVERSE"] {
assert!(
reasons.iter().any(|reason| reason.contains(required)),
"missing explicit unsupported boundary for {required}: {reasons:?}"
);
}
}
}