1use ifc_schema::{Schema, SchemaVersion};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Support {
29 Implemented,
31 Unsupported(&'static str),
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct RuleEntry {
46 pub id: &'static str,
50 pub entity: Option<&'static str>,
55 pub releases: &'static [SchemaVersion],
57 pub support: Support,
59}
60
61impl RuleEntry {
62 #[must_use]
68 pub fn applies_to(&self, schema: &Schema) -> bool {
69 schema
70 .version()
71 .is_some_and(|version| self.releases.contains(&version))
72 }
73}
74
75const NEEDS_EXPRESSIONS: &str = "requires an EXPRESS expression evaluator";
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] = &[
84 SchemaVersion::Ifc2x3,
85 SchemaVersion::Ifc4,
86 SchemaVersion::Ifc4x3,
87];
88const IFC2X3: &[SchemaVersion] = &[SchemaVersion::Ifc2x3];
90const IFC4_FAMILY: &[SchemaVersion] = &[SchemaVersion::Ifc4, SchemaVersion::Ifc4x3];
92
93pub const RULES: &[RuleEntry] = &[
102 RuleEntry {
103 id: "global.IfcSingleProjectInstance",
104 entity: None,
105 releases: ALL,
106 support: Support::Implemented,
107 },
108 RuleEntry {
112 id: "global.UniqueGlobalId",
113 entity: None,
114 releases: ALL,
115 support: Support::Implemented,
116 },
117 RuleEntry {
118 id: "IfcRelDefinesByProperties.NoRelatedTypeObject",
119 entity: Some("IfcRelDefinesByProperties"),
120 releases: IFC4_FAMILY,
121 support: Support::Implemented,
122 },
123 RuleEntry {
124 id: "IfcExternalReference.WR1",
125 entity: Some("IfcExternalReference"),
126 releases: ALL,
127 support: Support::Implemented,
128 },
129 RuleEntry {
132 id: "IfcRelSequence.WR1",
133 entity: Some("IfcRelSequence"),
134 releases: IFC2X3,
135 support: Support::Implemented,
136 },
137 RuleEntry {
138 id: "IfcRelSequence.AvoidInconsistentSequence",
139 entity: Some("IfcRelSequence"),
140 releases: IFC4_FAMILY,
141 support: Support::Implemented,
142 },
143 RuleEntry {
146 id: "IfcRelAggregates.NoSelfReference",
147 entity: Some("IfcRelAggregates"),
148 releases: IFC4_FAMILY,
149 support: Support::Implemented,
150 },
151 RuleEntry {
152 id: "IfcRelNests.NoSelfReference",
153 entity: Some("IfcRelNests"),
154 releases: IFC4_FAMILY,
155 support: Support::Implemented,
156 },
157 RuleEntry {
158 id: "IfcMaterialLayer.NormalizedPriority",
159 entity: Some("IfcMaterialLayer"),
160 releases: IFC4_FAMILY,
161 support: Support::Implemented,
162 },
163 RuleEntry {
164 id: "IfcRelAssignsToActor.NoSelfReference",
165 entity: Some("IfcRelAssignsToActor"),
166 releases: IFC4_FAMILY,
167 support: Support::Implemented,
168 },
169 RuleEntry {
170 id: "IfcRelAssignsToProcess.NoSelfReference",
171 entity: Some("IfcRelAssignsToProcess"),
172 releases: IFC4_FAMILY,
173 support: Support::Implemented,
174 },
175 RuleEntry {
176 id: "IfcRelAssignsToProduct.NoSelfReference",
177 entity: Some("IfcRelAssignsToProduct"),
178 releases: IFC4_FAMILY,
179 support: Support::Implemented,
180 },
181 RuleEntry {
184 id: "IfcRelAssignsToGroup.NoSelfReference",
185 entity: Some("IfcRelAssignsToGroup"),
186 releases: IFC4_FAMILY,
187 support: Support::Implemented,
188 },
189 RuleEntry {
190 id: "IfcRelConnectsPathElements.NormalizedRelatingPriorities",
191 entity: Some("IfcRelConnectsPathElements"),
192 releases: IFC4_FAMILY,
193 support: Support::Implemented,
194 },
195 RuleEntry {
196 id: "IfcRelConnectsPathElements.NormalizedRelatedPriorities",
197 entity: Some("IfcRelConnectsPathElements"),
198 releases: IFC4_FAMILY,
199 support: Support::Implemented,
200 },
201 RuleEntry {
202 id: "IfcRelSpaceBoundary.CorrectPhysOrVirt",
203 entity: Some("IfcRelSpaceBoundary"),
204 releases: IFC4_FAMILY,
205 support: Support::Implemented,
206 },
207 RuleEntry {
210 id: "IfcObject.UniquePropertySetNames",
211 entity: Some("IfcObject"),
212 releases: IFC4_FAMILY,
213 support: Support::Implemented,
214 },
215 RuleEntry {
216 id: "IfcTypeObject.UniquePropertySetNames",
217 entity: Some("IfcTypeObject"),
218 releases: IFC4_FAMILY,
219 support: Support::Implemented,
220 },
221 RuleEntry {
224 id: "IfcTypeProduct.WR41",
225 entity: Some("IfcTypeProduct"),
226 releases: IFC2X3,
227 support: Support::Implemented,
228 },
229 RuleEntry {
230 id: "IfcTypeProduct.ApplicableOccurrence",
231 entity: Some("IfcTypeProduct"),
232 releases: IFC4_FAMILY,
233 support: Support::Implemented,
234 },
235 RuleEntry {
236 id: "IfcDocumentReference.WR1",
237 entity: Some("IfcDocumentReference"),
238 releases: ALL,
239 support: Support::Unsupported(NEEDS_INVERSES),
240 },
241 RuleEntry {
242 id: "IfcRepresentationContextSameWCS",
243 entity: None,
244 releases: ALL,
245 support: Support::Unsupported(NEEDS_GEOMETRY),
246 },
247 RuleEntry {
250 id: "IfcPolyLoop.WR21",
251 entity: Some("IfcPolyLoop"),
252 releases: IFC2X3,
253 support: Support::Unsupported(NEEDS_EXPRESSIONS),
254 },
255 RuleEntry {
256 id: "IfcPolyLoop.AllPointsSameDim",
257 entity: Some("IfcPolyLoop"),
258 releases: IFC4_FAMILY,
259 support: Support::Unsupported(NEEDS_EXPRESSIONS),
260 },
261 RuleEntry {
262 id: "IfcQuantityLength.WR21",
263 entity: Some("IfcQuantityLength"),
264 releases: ALL,
265 support: Support::Unsupported(NEEDS_EXPRESSIONS),
266 },
267 RuleEntry {
268 id: "IfcZone.WR1",
269 entity: Some("IfcZone"),
270 releases: ALL,
271 support: Support::Unsupported(NEEDS_EXPRESSIONS),
272 },
273];
274
275#[must_use]
277pub fn lookup(id: &str) -> Option<&'static RuleEntry> {
278 RULES.iter().find(|entry| entry.id == id)
279}
280
281pub fn unsupported() -> impl Iterator<Item = &'static RuleEntry> {
283 RULES
284 .iter()
285 .filter(|entry| matches!(entry.support, Support::Unsupported(_)))
286}
287
288pub fn implemented() -> impl Iterator<Item = &'static RuleEntry> {
290 RULES
291 .iter()
292 .filter(|entry| matches!(entry.support, Support::Implemented))
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
301 fn rule_ids_are_unique() {
302 let mut ids: Vec<&str> = RULES.iter().map(|entry| entry.id).collect();
303 ids.sort_unstable();
304 let count = ids.len();
305 ids.dedup();
306 assert_eq!(ids.len(), count, "duplicate rule id in the registry");
307 }
308
309 #[test]
316 fn the_registry_admits_what_it_cannot_check() {
317 assert!(
318 unsupported().count() > 0,
319 "a validator claiming full WHERE-rule coverage is lying"
320 );
321 }
322
323 #[test]
330 fn entity_ids_name_their_declaring_entity_in_every_claimed_release() {
331 for entry in RULES {
332 assert!(!entry.releases.is_empty(), "{} applies nowhere", entry.id);
333 let Some(entity) = entry.entity else {
334 continue;
335 };
336 assert!(
337 entry.id.starts_with(&format!("{entity}.")),
338 "{} is not `{entity}.<label>`",
339 entry.id
340 );
341 for version in entry.releases {
342 let schema = ifc_schema::for_version(*version).expect("bundled tables");
343 assert!(
344 schema.entity(entity).is_some(),
345 "{} claims {version:?}, which declares no {entity}",
346 entry.id
347 );
348 }
349 }
350 }
351
352 #[test]
354 fn an_unknown_release_declares_no_registered_rule() {
355 let schema = Schema::from_express("SCHEMA IFC9;\nEND_SCHEMA;\n");
356 assert!(RULES.iter().all(|entry| !entry.applies_to(&schema)));
357 }
358
359 #[test]
360 fn unsupported_boundaries_remain_explicit() {
361 let reasons: Vec<_> = unsupported()
362 .filter_map(|entry| match entry.support {
363 Support::Unsupported(reason) => Some(reason),
364 Support::Implemented => None,
365 })
366 .collect();
367 assert!(
370 reasons
371 .iter()
372 .all(|reason| !reason.contains("aggregate bounds")),
373 "{reasons:?}"
374 );
375 for required in ["EXPRESS expression", "INVERSE"] {
376 assert!(
377 reasons.iter().any(|reason| reason.contains(required)),
378 "missing explicit unsupported boundary for {required}: {reasons:?}"
379 );
380 }
381 }
382}