1use ifc_schema::{Schema, SchemaVersion};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Support {
28 Implemented,
30 Unsupported(&'static str),
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct RuleEntry {
45 pub id: &'static str,
49 pub entity: Option<&'static str>,
54 pub releases: &'static [SchemaVersion],
56 pub support: Support,
58}
59
60impl RuleEntry {
61 #[must_use]
67 pub fn applies_to(&self, schema: &Schema) -> bool {
68 schema
69 .version()
70 .is_some_and(|version| self.releases.contains(&version))
71 }
72}
73
74const NEEDS_EXPRESSIONS: &str = "requires an EXPRESS expression evaluator";
76const NEEDS_BOUNDS: &str = "requires aggregate bounds, which the schema parser does not retain";
77const NEEDS_INVERSES: &str =
78 "not implemented uniformly: IFC2X3 requires INVERSE relationship semantics, which validation does not derive";
79const NEEDS_GEOMETRY: &str = "requires geometric evaluation, which validation does not perform";
80
81const ALL: &[SchemaVersion] = &[
83 SchemaVersion::Ifc2x3,
84 SchemaVersion::Ifc4,
85 SchemaVersion::Ifc4x3,
86];
87const IFC2X3: &[SchemaVersion] = &[SchemaVersion::Ifc2x3];
89const IFC4_FAMILY: &[SchemaVersion] = &[SchemaVersion::Ifc4, SchemaVersion::Ifc4x3];
91
92pub const RULES: &[RuleEntry] = &[
101 RuleEntry {
102 id: "global.IfcSingleProjectInstance",
103 entity: None,
104 releases: ALL,
105 support: Support::Implemented,
106 },
107 RuleEntry {
111 id: "global.UniqueGlobalId",
112 entity: None,
113 releases: ALL,
114 support: Support::Implemented,
115 },
116 RuleEntry {
117 id: "IfcRelDefinesByProperties.NoRelatedTypeObject",
118 entity: Some("IfcRelDefinesByProperties"),
119 releases: IFC4_FAMILY,
120 support: Support::Implemented,
121 },
122 RuleEntry {
123 id: "IfcExternalReference.WR1",
124 entity: Some("IfcExternalReference"),
125 releases: ALL,
126 support: Support::Implemented,
127 },
128 RuleEntry {
131 id: "IfcRelSequence.WR1",
132 entity: Some("IfcRelSequence"),
133 releases: IFC2X3,
134 support: Support::Implemented,
135 },
136 RuleEntry {
137 id: "IfcRelSequence.AvoidInconsistentSequence",
138 entity: Some("IfcRelSequence"),
139 releases: IFC4_FAMILY,
140 support: Support::Implemented,
141 },
142 RuleEntry {
145 id: "IfcRelAggregates.NoSelfReference",
146 entity: Some("IfcRelAggregates"),
147 releases: IFC4_FAMILY,
148 support: Support::Implemented,
149 },
150 RuleEntry {
151 id: "IfcRelNests.NoSelfReference",
152 entity: Some("IfcRelNests"),
153 releases: IFC4_FAMILY,
154 support: Support::Implemented,
155 },
156 RuleEntry {
157 id: "IfcMaterialLayer.NormalizedPriority",
158 entity: Some("IfcMaterialLayer"),
159 releases: IFC4_FAMILY,
160 support: Support::Implemented,
161 },
162 RuleEntry {
163 id: "IfcRelAssignsToActor.NoSelfReference",
164 entity: Some("IfcRelAssignsToActor"),
165 releases: IFC4_FAMILY,
166 support: Support::Implemented,
167 },
168 RuleEntry {
169 id: "IfcRelAssignsToProcess.NoSelfReference",
170 entity: Some("IfcRelAssignsToProcess"),
171 releases: IFC4_FAMILY,
172 support: Support::Implemented,
173 },
174 RuleEntry {
175 id: "IfcRelAssignsToProduct.NoSelfReference",
176 entity: Some("IfcRelAssignsToProduct"),
177 releases: IFC4_FAMILY,
178 support: Support::Implemented,
179 },
180 RuleEntry {
183 id: "IfcRelAssignsToGroup.NoSelfReference",
184 entity: Some("IfcRelAssignsToGroup"),
185 releases: IFC4_FAMILY,
186 support: Support::Implemented,
187 },
188 RuleEntry {
189 id: "IfcRelConnectsPathElements.NormalizedRelatingPriorities",
190 entity: Some("IfcRelConnectsPathElements"),
191 releases: IFC4_FAMILY,
192 support: Support::Implemented,
193 },
194 RuleEntry {
195 id: "IfcRelConnectsPathElements.NormalizedRelatedPriorities",
196 entity: Some("IfcRelConnectsPathElements"),
197 releases: IFC4_FAMILY,
198 support: Support::Implemented,
199 },
200 RuleEntry {
201 id: "IfcRelSpaceBoundary.CorrectPhysOrVirt",
202 entity: Some("IfcRelSpaceBoundary"),
203 releases: IFC4_FAMILY,
204 support: Support::Implemented,
205 },
206 RuleEntry {
207 id: "IfcDocumentReference.WR1",
208 entity: Some("IfcDocumentReference"),
209 releases: ALL,
210 support: Support::Unsupported(NEEDS_INVERSES),
211 },
212 RuleEntry {
213 id: "IfcRepresentationContextSameWCS",
214 entity: None,
215 releases: ALL,
216 support: Support::Unsupported(NEEDS_GEOMETRY),
217 },
218 RuleEntry {
221 id: "IfcPolyLoop.WR21",
222 entity: Some("IfcPolyLoop"),
223 releases: IFC2X3,
224 support: Support::Unsupported(NEEDS_BOUNDS),
225 },
226 RuleEntry {
227 id: "IfcPolyLoop.AllPointsSameDim",
228 entity: Some("IfcPolyLoop"),
229 releases: IFC4_FAMILY,
230 support: Support::Unsupported(NEEDS_BOUNDS),
231 },
232 RuleEntry {
233 id: "IfcQuantityLength.WR21",
234 entity: Some("IfcQuantityLength"),
235 releases: ALL,
236 support: Support::Unsupported(NEEDS_EXPRESSIONS),
237 },
238 RuleEntry {
239 id: "IfcZone.WR1",
240 entity: Some("IfcZone"),
241 releases: ALL,
242 support: Support::Unsupported(NEEDS_EXPRESSIONS),
243 },
244];
245
246#[must_use]
248pub fn lookup(id: &str) -> Option<&'static RuleEntry> {
249 RULES.iter().find(|entry| entry.id == id)
250}
251
252pub fn unsupported() -> impl Iterator<Item = &'static RuleEntry> {
254 RULES
255 .iter()
256 .filter(|entry| matches!(entry.support, Support::Unsupported(_)))
257}
258
259pub fn implemented() -> impl Iterator<Item = &'static RuleEntry> {
261 RULES
262 .iter()
263 .filter(|entry| matches!(entry.support, Support::Implemented))
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
272 fn rule_ids_are_unique() {
273 let mut ids: Vec<&str> = RULES.iter().map(|entry| entry.id).collect();
274 ids.sort_unstable();
275 let count = ids.len();
276 ids.dedup();
277 assert_eq!(ids.len(), count, "duplicate rule id in the registry");
278 }
279
280 #[test]
287 fn the_registry_admits_what_it_cannot_check() {
288 assert!(
289 unsupported().count() > 0,
290 "a validator claiming full WHERE-rule coverage is lying"
291 );
292 }
293
294 #[test]
301 fn entity_ids_name_their_declaring_entity_in_every_claimed_release() {
302 for entry in RULES {
303 assert!(!entry.releases.is_empty(), "{} applies nowhere", entry.id);
304 let Some(entity) = entry.entity else {
305 continue;
306 };
307 assert!(
308 entry.id.starts_with(&format!("{entity}.")),
309 "{} is not `{entity}.<label>`",
310 entry.id
311 );
312 for version in entry.releases {
313 let schema = ifc_schema::for_version(*version).expect("bundled tables");
314 assert!(
315 schema.entity(entity).is_some(),
316 "{} claims {version:?}, which declares no {entity}",
317 entry.id
318 );
319 }
320 }
321 }
322
323 #[test]
325 fn an_unknown_release_declares_no_registered_rule() {
326 let schema = Schema::from_express("SCHEMA IFC9;\nEND_SCHEMA;\n");
327 assert!(RULES.iter().all(|entry| !entry.applies_to(&schema)));
328 }
329
330 #[test]
331 fn unsupported_boundaries_remain_explicit() {
332 let reasons: Vec<_> = unsupported()
333 .filter_map(|entry| match entry.support {
334 Support::Unsupported(reason) => Some(reason),
335 Support::Implemented => None,
336 })
337 .collect();
338 for required in ["aggregate bounds", "EXPRESS expression", "INVERSE"] {
339 assert!(
340 reasons.iter().any(|reason| reason.contains(required)),
341 "missing explicit unsupported boundary for {required}: {reasons:?}"
342 );
343 }
344 }
345}